Visualisation d'un parcours et ses UEs (WIP)

This commit is contained in:
Emmanuel Viennet 2023-04-07 17:10:17 +02:00
parent 6e86f7a9c4
commit d307fcb1e9
8 changed files with 355 additions and 25 deletions

View File

@ -290,7 +290,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
ues_parcour = self.formsemestre.formation.query_ues_parcour(parcour) ues_parcour = self.formsemestre.formation.query_ues_parcour(parcour)
ues_ids = set() ues_ids = set()
for niveau in niveaux: for niveau in niveaux:
ue = ues_parcour.filter_by(UniteEns.niveau_competence == niveau).first() ue = ues_parcour.filter(UniteEns.niveau_competence == niveau).first()
if ue: if ue:
ues_ids.add(ue.id) ues_ids.add(ue.id)

View File

@ -383,9 +383,12 @@ class ApcNiveau(db.Model, XMLModel):
parcour: "ApcParcours", parcour: "ApcParcours",
annee: int, annee: int,
referentiel_competence: ApcReferentielCompetences = None, referentiel_competence: ApcReferentielCompetences = None,
competence: ApcCompetence = None,
) -> list["ApcNiveau"]: ) -> list["ApcNiveau"]:
"""Les niveaux de l'année du parcours """Les niveaux de l'année du parcours
Si le parcour est None, tous les niveaux de l'année Si le parcour est None, tous les niveaux de l'année
(dans ce cas, spécifier referentiel_competence)
Si competence est indiquée, filtre les niveaux de cette compétence.
""" """
if annee not in {1, 2, 3}: if annee not in {1, 2, 3}:
raise ValueError("annee invalide pour un parcours BUT") raise ValueError("annee invalide pour un parcours BUT")
@ -396,15 +399,20 @@ class ApcNiveau(db.Model, XMLModel):
raise ScoNoReferentielCompetences() raise ScoNoReferentielCompetences()
if not parcour: if not parcour:
annee_formation = f"BUT{annee}" annee_formation = f"BUT{annee}"
return ApcNiveau.query.filter( query = ApcNiveau.query.filter(
ApcNiveau.annee == annee_formation, ApcNiveau.annee == annee_formation,
ApcCompetence.id == ApcNiveau.competence_id, ApcCompetence.id == ApcNiveau.competence_id,
ApcCompetence.referentiel_id == referentiel_competence.id, ApcCompetence.referentiel_id == referentiel_competence.id,
) )
annee_parcour = parcour.annees.filter_by(ordre=annee).first() if competence is not None:
query = query.filter(ApcCompetence.id == competence.id)
return query.all()
annee_parcour: ApcAnneeParcours = parcour.annees.filter_by(ordre=annee).first()
if not annee_parcour: if not annee_parcour:
return [] return []
if competence is None:
parcour_niveaux: list[ parcour_niveaux: list[
ApcParcoursNiveauCompetence ApcParcoursNiveauCompetence
] = annee_parcour.niveaux_competences ] = annee_parcour.niveaux_competences
@ -412,6 +420,10 @@ class ApcNiveau(db.Model, XMLModel):
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
] ]
else:
niveaux: list[ApcNiveau] = competence.niveaux.filter_by(
annee=f"BUT{int(annee)}"
).all()
return niveaux return niveaux
@ -558,6 +570,16 @@ class ApcParcours(db.Model, XMLModel):
.order_by(ApcCompetence.numero) .order_by(ApcCompetence.numero)
) )
def get_competence_by_titre(self, titre: str) -> ApcCompetence:
"La compétence de titre donné dans ce parcours, ou None"
return (
ApcCompetence.query.filter_by(titre=titre)
.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
.filter_by(parcours_id=self.id)
.order_by(ApcCompetence.numero)
.first()
)
class ApcAnneeParcours(db.Model, XMLModel): class ApcAnneeParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)

View File

@ -0,0 +1,68 @@
.parcour_formation {
margin-left: 24px;
width: 990px;
}
.titre_parcours {
font-weight: bold;
font-size: 120%;
}
div.competence {
/* display: grid; */
margin-top: 12px;
}
.titre_competence {
/* grid-column-start: 1;
grid-column-end: span -1;
grid-row-start: 1;
grid-row-start: 2; */
border-bottom: 6px solid white;
font-weight: bold;
font-size: 110%;
text-align: center;
}
.niveaux {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
.niveau {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: auto auto;
}
.niveau>div {
padding-left: 8px;
padding-right: 8px;
}
.titre_niveau {
grid-column: 1 / span 2;
grid-row: 1 / 2;
}
div.ue {
grid-row-start: 2;
/* border: 1px dashed blue; */
}
div.ue.impair {
grid-column: 1 / 2;
}
div.ue.pair {
grid-column: 2 / 3;
}
.niveau-1 {
opacity: 0.4;
}
.niveau-2 {
opacity: 0.7;
}

View File

@ -56,26 +56,90 @@ table.table_niveaux_parcours tr.annee_but td.empty {
opacity: 0; opacity: 0;
} }
/* Les couleurs des niveaux de compétences du BO */
.comp-c1-1 {
background: rgb(224, 201, 201);
color: black;
}
.comp-c1-2 {
background: rgb(231, 127, 130);
color: black;
}
.comp-c1-3,
.comp-c1 { .comp-c1 {
background: #a44 background: rgb(167, 0, 9);
color: #eee;
} }
.comp-c2-1 {
background: rgb(240, 218, 198);
}
.comp-c2-2 {
background: rgb(231, 142, 95);
}
.comp-c2-3,
.comp-c2 { .comp-c2 {
background: #84a background: rgb(231, 119, 64);
} }
.comp-c3-1 {
background: rgb(241, 227, 167);
}
.comp-c3-2 {
background: rgb(238, 208, 86);
}
.comp-c3-3,
.comp-c3 { .comp-c3 {
background: #a84 background: rgb(233, 174, 17);
} }
.comp-c4-1 {
background: rgb(218, 225, 205);
}
.comp-c4-2 {
background: rgb(159, 207, 111);
}
.comp-c4-3,
.comp-c4 { .comp-c4 {
background: #8a4 background: rgb(124, 192, 64);
} }
.comp-c5-1 {
background: rgb(191, 206, 230);
color: black;
}
.comp-c5-2 {
background: rgb(119, 156, 208);
color: black;
}
.comp-c5-3,
.comp-c5 { .comp-c5 {
background: #4a8 background: rgb(10, 22, 75);
color: #eee;
} }
.comp-c6-1,
.comp-c6 { .comp-c6 {
background: #48a background: rgb(203, 199, 176);
color: black;
}
.comp-c6-2 {
background: rgb(152, 143, 97);
color: black;
}
.comp-c6-3 {
background: rgb(13, 13, 13);
color: #eee;
} }

View File

@ -0,0 +1,34 @@
{% extends "sco_page.j2" %}
{% block styles %}
{{super()}}
<link href="{{sco.scu.STATIC_DIR}}/css/refcomp_parcours_niveaux.css" rel="stylesheet" type="text/css" />
<link href="{{sco.scu.STATIC_DIR}}/css/parcour_formation.css" rel="stylesheet" type="text/css" />
{% endblock %}
{% block app_content %}
<div class="parcour_formation">
<div class="titre_parcours">Parcours {{parcour.code}} « {{parcour.libelle}} »</div>
{% for comp in competences_parcour %}
{% set color_idx = 1 + loop.index0 % 6 %}
<div class="competence comp-c{{color_idx}}">
<div class="titre_competence tc">
Compétence {{comp['competence'].numero}}&nbsp;: {{comp['competence'].titre}}
</div>
<div class="niveaux">
{% for annee, niv in comp['niveaux'].items() %}
<div class="niveau comp-c{{color_idx}}-{{annee}}">
<div class="titre_niveau n{{annee}}">{{niv['niveau'].libelle if niv['niveau'] else '-'}}</div>
<div class="ue impair u{{annee}}1">{{niv['ue_impair'].acronyme if niv['ue_impair'] else 'UE1'}}</div>
<div class="ue pair u{{annee}}1">{{niv['ue_pair'].acronyme if niv['ue_pair'] else 'UE2'}}</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -107,6 +107,7 @@ class ScoData:
from app.views import ( from app.views import (
absences, absences,
but_formation,
notes_formsemestre, notes_formsemestre,
notes, notes,
pn_modules, pn_modules,

145
app/views/but_formation.py Normal file
View File

@ -0,0 +1,145 @@
##############################################################################
#
# ScoDoc
#
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""
Vues sur les formations BUT
Emmanuel Viennet, 2023
"""
from flask import g, render_template
from app import log
from app.decorators import (
scodoc,
permission_required,
)
from app.models import (
ApcCompetence,
ApcNiveau,
ApcParcours,
ApcReferentielCompetences,
Formation,
)
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError
from app.views import notes_bp as bp
from app.views import ScoData
@bp.route("/parcour_formation/<int:parcour_id>/<int:formation_id>")
@scodoc
@permission_required(Permission.ScoView)
def parcour_formation(parcour_id: int, formation_id: int) -> str:
"""visu HTML d'un parcours dans une formation,
avec les compétences, niveaux et UEs associées."""
formation: Formation = Formation.query.filter_by(
id=formation_id, dept_id=g.scodoc_dept_id
).first_or_404()
ref_comp: ApcReferentielCompetences = formation.referentiel_competence
if ref_comp is None:
return "pas de référentiel de compétences"
parcour: ApcParcours = ref_comp.parcours.filter_by(id=parcour_id).first()
if parcour is None:
raise ScoValueError("parcours invalide ou hors référentiel de formation")
competences_parcour = parcour_formation_competences(parcour, formation)
return render_template(
"but/parcour_formation.j2",
formation=formation,
parcour=parcour,
competences_parcour=competences_parcour,
sco=ScoData(),
)
def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> list:
"""
[
{
'competence' : ApcCompetence,
'niveaux' : {
1 : { ... },
2 : { ... },
3 : {
'niveau' : ApcNiveau,
'ue_impair' : UniteEns,
'ue_pair' : UniteEns
}
}
}
]
"""
def _niveau_ues(competence: ApcCompetence, annee: int) -> dict:
niveaux = ApcNiveau.niveaux_annee_de_parcours(
parcour, annee, competence=competence
)
if len(niveaux) > 0:
if len(niveaux) > 1:
log(f"_niveau_ues: plus d'un niveau pour {competence} annee {annee}")
niveau = niveaux[0]
elif len(niveaux) == 0:
return {"niveau": None, "ue_pair": None, "ue_impair": None}
ues = [
ue
for ue in niveau.ues
if ue.formation.id == formation.id
and parcour.id in (p.id for p in ue.parcours)
]
ues_pair = [ue for ue in ues if ue.semestre_idx == 2 * annee]
if len(ues_pair) > 0:
ue_pair = ues_pair[0]
if len(ues_pair) > 1:
log(
f"_niveau_ues: {len(ues)} associées au niveau {niveau} / S{2*annee}"
)
else:
ue_pair = None
ues_impair = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)]
if len(ues_impair) > 0:
ue_impair = ues_impair[0]
if len(ues_impair) > 1:
log(
f"_niveau_ues: {len(ues)} associées au niveau {niveau} / S{2*annee-1}"
)
else:
ue_impair = None
return {
"niveau": niveau,
"ue_pair": ue_pair,
"ue_impair": ue_impair,
}
competences = [
{
"competence": competence,
"niveaux": {annee: _niveau_ues(competence, annee) for annee in (1, 2, 3)},
}
for competence in parcour.query_competences()
]
return competences

View File

@ -263,16 +263,12 @@ def refcomp_load(formation_id=None):
category="info", category="info",
) )
if formation is not None:
return redirect( return redirect(
url_for( url_for(
"notes.refcomp_assoc_formation", "notes.refcomp_table",
scodoc_dept=g.scodoc_dept, 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( return render_template(
"but/refcomp_load.j2", "but/refcomp_load.j2",