diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 71bd71a8..7e1d481a 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -290,7 +290,7 @@ class ResultatsSemestreBUT(NotesTableCompat): ues_parcour = self.formsemestre.formation.query_ues_parcour(parcour) ues_ids = set() 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: ues_ids.add(ue.id) diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 420f96ae..7c22f53f 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -383,9 +383,12 @@ class ApcNiveau(db.Model, XMLModel): parcour: "ApcParcours", annee: int, referentiel_competence: ApcReferentielCompetences = None, + competence: ApcCompetence = None, ) -> list["ApcNiveau"]: """Les niveaux de l'année du parcours 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}: raise ValueError("annee invalide pour un parcours BUT") @@ -396,22 +399,31 @@ class ApcNiveau(db.Model, XMLModel): raise ScoNoReferentielCompetences() if not parcour: annee_formation = f"BUT{annee}" - return ApcNiveau.query.filter( + query = ApcNiveau.query.filter( ApcNiveau.annee == annee_formation, ApcCompetence.id == ApcNiveau.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: return [] - parcour_niveaux: list[ - ApcParcoursNiveauCompetence - ] = annee_parcour.niveaux_competences - niveaux: list[ApcNiveau] = [ - pn.competence.niveaux.filter_by(ordre=pn.niveau).first() - for pn in parcour_niveaux - ] + if competence is None: + parcour_niveaux: list[ + ApcParcoursNiveauCompetence + ] = annee_parcour.niveaux_competences + niveaux: list[ApcNiveau] = [ + pn.competence.niveaux.filter_by(ordre=pn.niveau).first() + for pn in parcour_niveaux + ] + else: + niveaux: list[ApcNiveau] = competence.niveaux.filter_by( + annee=f"BUT{int(annee)}" + ).all() return niveaux @@ -558,6 +570,16 @@ class ApcParcours(db.Model, XMLModel): .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): id = db.Column(db.Integer, primary_key=True) diff --git a/app/static/css/parcour_formation.css b/app/static/css/parcour_formation.css new file mode 100644 index 00000000..a344d2f5 --- /dev/null +++ b/app/static/css/parcour_formation.css @@ -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; +} \ No newline at end of file diff --git a/app/static/css/refcomp_parcours_niveaux.css b/app/static/css/refcomp_parcours_niveaux.css index 39c47b07..081f7f35 100644 --- a/app/static/css/refcomp_parcours_niveaux.css +++ b/app/static/css/refcomp_parcours_niveaux.css @@ -56,26 +56,90 @@ table.table_niveaux_parcours tr.annee_but td.empty { 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 { - 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 { - 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 { - 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 { - 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 { - background: #4a8 + background: rgb(10, 22, 75); + color: #eee; } +.comp-c6-1, .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; } \ No newline at end of file diff --git a/app/templates/but/parcour_formation.j2 b/app/templates/but/parcour_formation.j2 new file mode 100644 index 00000000..26351c9b --- /dev/null +++ b/app/templates/but/parcour_formation.j2 @@ -0,0 +1,34 @@ +{% extends "sco_page.j2" %} + +{% block styles %} + {{super()}} + + +{% endblock %} + +{% block app_content %} +
+ +
Parcours {{parcour.code}} « {{parcour.libelle}} »
+ +{% for comp in competences_parcour %} +{% set color_idx = 1 + loop.index0 % 6 %} +
+
+ Compétence {{comp['competence'].numero}} : {{comp['competence'].titre}} +
+
+ {% for annee, niv in comp['niveaux'].items() %} +
+
{{niv['niveau'].libelle if niv['niveau'] else '-'}}
+
{{niv['ue_impair'].acronyme if niv['ue_impair'] else 'UE1'}}
+
{{niv['ue_pair'].acronyme if niv['ue_pair'] else 'UE2'}}
+
+ {% endfor %} +
+
+{% endfor %} + +
+ +{% endblock %} \ No newline at end of file diff --git a/app/views/__init__.py b/app/views/__init__.py index b33faf3e..9d342dbb 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -107,6 +107,7 @@ class ScoData: from app.views import ( absences, + but_formation, notes_formsemestre, notes, pn_modules, diff --git a/app/views/but_formation.py b/app/views/but_formation.py new file mode 100644 index 00000000..c73f7304 --- /dev/null +++ b/app/views/but_formation.py @@ -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//") +@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 diff --git a/app/views/refcomp.py b/app/views/refcomp.py index ad047dba..83f6645f 100644 --- a/app/views/refcomp.py +++ b/app/views/refcomp.py @@ -263,16 +263,12 @@ def refcomp_load(formation_id=None): category="info", ) - if formation is not None: - return redirect( - url_for( - "notes.refcomp_assoc_formation", - scodoc_dept=g.scodoc_dept, - formation_id=formation.formation_id, - ) + return redirect( + url_for( + "notes.refcomp_table", + scodoc_dept=g.scodoc_dept, ) - else: - return redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept)) + ) return render_template( "but/refcomp_load.j2",