From f08a4130dd3ef6feefba5870e1bba488ac867dd7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 16 Jul 2023 19:59:45 +0200 Subject: [PATCH] =?UTF-8?q?Nouvelle=20page=20de=20visu/saisie=20des=20d?= =?UTF-8?q?=C3=A9cisions=20RCUEs:=20validation=5Frcues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/jury.py | 141 ++++++++++++++++- app/api/partitions.py | 2 +- app/but/cursus_but.py | 124 ++++++++++++++- app/but/validations_view.py | 115 ++++++++++++++ app/scodoc/codes_cursus.py | 1 + app/scodoc/sco_page_etud.py | 23 ++- app/static/css/parcour_formation.css | 11 +- app/templates/but/validation_rcues.j2 | 218 ++++++++++++++++++++++++++ app/views/__init__.py | 3 +- app/views/but_formation.py | 132 ++++------------ tests/api/test_api_jury.py | 28 +++- 11 files changed, 687 insertions(+), 111 deletions(-) create mode 100644 app/but/validations_view.py create mode 100644 app/templates/but/validation_rcues.j2 diff --git a/app/api/jury.py b/app/api/jury.py index 2a86766c97..450edde7de 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -8,25 +8,32 @@ ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions """ -from flask import g, url_for +import datetime + +from flask import flash, g, request, url_for from flask_json import as_json from flask_login import current_user, login_required import app from app import db, log -from app.api import api_bp as bp, api_web_bp, tools +from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR, tools from app.decorators import scodoc, permission_required from app.scodoc.sco_exceptions import ScoException from app.but import jury_but_results from app.models import ( + ApcParcours, ApcValidationAnnee, ApcValidationRCUE, + Formation, FormSemestre, Identite, ScolarAutorisationInscription, ScolarFormSemestreValidation, ScolarNews, + Scolog, + UniteEns, ) +from app.scodoc import codes_cursus from app.scodoc import sco_cache from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error @@ -161,6 +168,136 @@ def autorisation_inscription_delete(etudid: int, validation_id: int): return "ok" +@bp.route( + "/etudiant//jury/validation_rcue/record", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant//jury/validation_rcue/record", + methods=["POST"], +) +@login_required +@scodoc +@permission_required(Permission.ScoEtudInscrit) +@as_json +def validation_rcue_record(etudid: int): + """Enregistre une validation de RCUE. + Si une validation existe déjà pour ce RCUE, la remplace. + The request content type should be "application/json": + { + "code" : str, + "ue1_id" : int, + "ue2_id" : int, + // Optionnel: + "formsemestre_id" : int, + "date" : date_iso, // si non spécifié, now() + "parcours_id" :int, + } + """ + etud = tools.get_etud(etudid) + if etud is None: + return json_error(404, "étudiant inconnu") + data = request.get_json(force=True) # may raise 400 Bad Request + code = data.get("code") + if code is None: + return json_error(API_CLIENT_ERROR, "missing argument: code") + if code not in codes_cursus.CODES_JURY_RCUE: + return json_error(API_CLIENT_ERROR, "invalid code value") + ue1_id = data.get("ue1_id") + if ue1_id is None: + return json_error(API_CLIENT_ERROR, "missing argument: ue1_id") + try: + ue1_id = int(ue1_id) + except ValueError: + return json_error(API_CLIENT_ERROR, "invalid value for ue1_id") + ue2_id = data.get("ue2_id") + if ue2_id is None: + return json_error(API_CLIENT_ERROR, "missing argument: ue2_id") + try: + ue2_id = int(ue2_id) + except ValueError: + return json_error(API_CLIENT_ERROR, "invalid value for ue2_id") + formsemestre_id = data.get("formsemestre_id") + date_validation_str = data.get("date", datetime.datetime.now().isoformat()) + parcours_id = data.get("parcours_id") + # + query = UniteEns.query.filter_by(id=ue1_id) + if g.scodoc_dept: + query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) + ue1: UniteEns = query.first_or_404() + query = UniteEns.query.filter_by(id=ue2_id) + if g.scodoc_dept: + query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) + ue2: UniteEns = query.first_or_404() + if ue1.niveau_competence_id != ue2.niveau_competence_id: + return json_error( + API_CLIENT_ERROR, "UEs non associees au meme niveau de competence" + ) + if formsemestre_id is not None: + query = FormSemestre.query.filter_by(id=formsemestre_id) + if g.scodoc_dept: + query = query.filter_by(dept_id=g.scodoc_dept_id) + formsemestre: FormSemestre = query.first_or_404() + if (formsemestre.formation_id != ue1.formation_id) or ( + formsemestre.formation_id != ue2.formation_id + ): + return json_error( + API_CLIENT_ERROR, "ues et semestre ne sont pas de la meme formation" + ) + else: + formsemestre = None + try: + date_validation = datetime.datetime.fromisoformat(date_validation_str) + except ValueError: + return json_error(API_CLIENT_ERROR, "invalid date string") + if parcours_id is not None: + parcours: ApcParcours = ApcParcours.query.get_or_404(parcours_id) + if parcours.referentiel_id != ue1.niveau_competence.competence.referentiel_id: + return json_error(API_CLIENT_ERROR, "niveau et parcours incompatibles") + + # Une validation pour ce niveau de compétence existe-elle ? + validation = ( + ApcValidationRCUE.query.filter_by(etudid=etudid) + .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id) + .filter_by(niveau_competence_id=ue2.niveau_competence_id) + .first() + ) + if validation: + validation.code = code + validation.date = date_validation + validation.formsemestre_id = formsemestre_id + validation.parcours_id = parcours_id + validation.ue1_id = ue1_id + validation.ue2_id = ue2_id + log(f"updating {validation}") + Scolog.logdb( + method="validation_rcue_record", + etudid=etudid, + msg=f"Mise à jour {validation}", + commit=False, + ) + else: + validation = ApcValidationRCUE( + code=code, + date=date_validation, + etudid=etudid, + formsemestre_id=formsemestre_id, + parcours_id=parcours_id, + ue1_id=ue1_id, + ue2_id=ue2_id, + ) + log(f"recording {validation}") + Scolog.logdb( + method="validation_rcue_record", + etudid=etudid, + msg=f"Enregistrement {validation}", + commit=False, + ) + db.session.add(validation) + db.session.commit() + return validation.to_dict() + + @bp.route( "/etudiant//jury/validation_rcue//delete", methods=["POST"], diff --git a/app/api/partitions.py b/app/api/partitions.py index f854af3822..2be45abc3d 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -282,7 +282,7 @@ def partition_remove_etud(partition_id: int, etudid: int): @scodoc @permission_required(Permission.ScoEtudChangeGroups) @as_json -def group_create(partition_id: int): +def group_create(partition_id: int): # partition-group-create """Création d'un groupe dans une partition The request content type should be "application/json": diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index 748908be79..f8b28675f9 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -18,7 +18,7 @@ from operator import attrgetter from flask import g, url_for -from app import db +from app import db, log from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_compat import NotesTableCompat @@ -41,7 +41,8 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation from app.scodoc import codes_cursus as sco_codes -from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES +from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD + from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError @@ -479,3 +480,122 @@ def formsemestre_warning_apc_setup(

""" + + +def ue_associee_au_niveau_du_parcours( + ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S" +) -> UniteEns: + "L'UE associée à ce niveau, ou None" + ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id] + if len(ues) > 1: + # plusieurs UEs associées à ce niveau: élimine celles sans parcours + ues_pair_avec_parcours = [ue for ue in ues if ue.parcours] + if ues_pair_avec_parcours: + ues = ues_pair_avec_parcours + if len(ues) > 1: + log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}") + return ues[0] if ues else None + + +def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> list: + """ + [ + { + 'competence' : ApcCompetence, + 'niveaux' : { + 1 : { ... }, + 2 : { ... }, + 3 : { + 'niveau' : ApcNiveau, + 'ue_impair' : UniteEns, # actuellement associée + 'ues_impair' : list[UniteEns], # choix possibles + 'ue_pair' : UniteEns, + 'ues_pair' : list[UniteEns], + } + } + } + ] + """ + refcomp: ApcReferentielCompetences = formation.referentiel_competence + + def _niveau_ues(competence: ApcCompetence, annee: int) -> dict: + """niveau et ues pour cette compétence de cette année du parcours. + Si parcour est None, les niveaux du tronc commun + """ + if parcour is not None: + # L'étudiant est inscrit à un parcours: cherche les niveaux + niveaux = ApcNiveau.niveaux_annee_de_parcours( + parcour, annee, competence=competence + ) + else: + # sans parcours, on cherche les niveaux du Tronc Commun de cette année + niveaux = [ + niveau + for niveau in refcomp.get_niveaux_by_parcours(annee)[1]["TC"] + if niveau.competence_id == competence.id + ] + + if len(niveaux) > 0: + if len(niveaux) > 1: + log( + f"""_niveau_ues: plus d'un niveau pour {competence} + annee {annee} {("parcours " + parcour.code) if parcour else ""}""" + ) + niveau = niveaux[0] + elif len(niveaux) == 0: + return { + "niveau": None, + "ue_pair": None, + "ue_impair": None, + "ues_pair": [], + "ues_impair": [], + } + # Toutes les UEs de la formation dans ce parcours ou tronc commun + ues = [ + ue + for ue in formation.ues + if ( + (not ue.parcours) + or (parcour is not None and (parcour.id in (p.id for p in ue.parcours))) + ) + and ue.type == UE_STANDARD + ] + ues_pair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee)] + ues_impair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)] + + # UE associée au niveau dans ce parcours + ue_pair = ue_associee_au_niveau_du_parcours( + ues_pair_possibles, niveau, f"S{2*annee}" + ) + ue_impair = ue_associee_au_niveau_du_parcours( + ues_impair_possibles, niveau, f"S{2*annee-1}" + ) + + return { + "niveau": niveau, + "ue_pair": ue_pair, + "ues_pair": [ + ue + for ue in ues_pair_possibles + if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id + ], + "ue_impair": ue_impair, + "ues_impair": [ + ue + for ue in ues_impair_possibles + if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id + ], + } + + competences = [ + { + "competence": competence, + "niveaux": {annee: _niveau_ues(competence, annee) for annee in (1, 2, 3)}, + } + for competence in ( + parcour.query_competences() + if parcour + else refcomp.competences.order_by(ApcCompetence.numero) + ) + ] + return competences diff --git a/app/but/validations_view.py b/app/but/validations_view.py new file mode 100644 index 0000000000..94ffb2f4ee --- /dev/null +++ b/app/but/validations_view.py @@ -0,0 +1,115 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes) + +Non spécifique au BUT. +""" + +from flask import render_template +import sqlalchemy as sa + +from app import log +from app.but import cursus_but +from app.models import ( + ApcCompetence, + ApcNiveau, + ApcReferentielCompetences, + # ApcValidationAnnee, # TODO + ApcValidationRCUE, + Formation, + FormSemestre, + Identite, + UniteEns, + # ScolarAutorisationInscription, + ScolarFormSemestreValidation, +) +from app.scodoc import codes_cursus +from app.scodoc.sco_exceptions import ScoNoReferentielCompetences +from app.views import ScoData + + +def validation_rcues(etud: Identite, formsemestre: FormSemestre, edit: bool = False): + """Page de saisie des décisions de RCUEs "antérieures" + On peut l'utiliser pour saisir la validation de n'importe quel RCUE + d'une année antérieure et de la formation du formsemestre indiqué. + """ + formation: Formation = formsemestre.formation + refcomp = formation.referentiel_competence + if refcomp is None: + raise ScoNoReferentielCompetences(formation=formation) + parcour = formsemestre.etuds_inscriptions[etud.id].parcour + # Si non inscrit à un parcours, prend toutes les compétences + competences_parcour = cursus_but.parcour_formation_competences(parcour, formation) + + ue_validation_by_niveau = get_ue_validation_by_niveau(refcomp, etud) + rcue_validation_by_niveau = get_rcue_validation_by_niveau(refcomp, etud) + return render_template( + "but/validation_rcues.j2", + competences_parcour=competences_parcour, + edit=edit, + formation=formation, + parcour=parcour, + rcue_validation_by_niveau=rcue_validation_by_niveau, + rcue_codes=sorted(codes_cursus.CODES_JURY_RCUE), + sco=ScoData(formsemestre=formsemestre, etud=etud), + title=f"{formation.acronyme} - Niveaux et UEs", + ue_validation_by_niveau=ue_validation_by_niveau, + ) + + +def get_ue_validation_by_niveau( + refcomp: ApcReferentielCompetences, etud: Identite +) -> dict[tuple[int, str], ScolarFormSemestreValidation]: + """Les validations d'UEs de cet étudiant liées à ce référentiel de compétences. + Pour chaque niveau / pair ou impair, choisi la "meilleure" validation + """ + validations: list[ScolarFormSemestreValidation] = ( + ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) + .join(UniteEns) + .join(ApcNiveau) + .join(ApcCompetence) + .filter_by(referentiel_id=refcomp.id) + .all() + ) + # La meilleure validation pour chaque UE + ue_validation_by_niveau = {} # { (niveau_id, pair|impair) : validation } + for validation in validations: + if validation.ue.niveau_competence is None: + log( + f"""validation_rcues: ignore validation d'UE { + validation.ue.id} pas de niveau de competence""" + ) + key = ( + validation.ue.niveau_competence.id, + "impair" if validation.ue.semestre_idx % 2 else "pair", + ) + existing = ue_validation_by_niveau.get(key, None) + if (not existing) or ( + codes_cursus.BUT_CODES_ORDER[existing.code] + < codes_cursus.BUT_CODES_ORDER[validation.code] + ): + ue_validation_by_niveau[key] = validation + return ue_validation_by_niveau + + +def get_rcue_validation_by_niveau( + refcomp: ApcReferentielCompetences, etud: Identite +) -> dict[int, ApcValidationRCUE]: + """Les validations d'UEs de cet étudiant liées à ce référentiel de compétences. + Pour chaque niveau / pair ou impair, choisi la "meilleure" validation + """ + validations: list[ApcValidationRCUE] = ( + ApcValidationRCUE.query.filter_by(etudid=etud.id) + .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id) + .join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id) + .join(ApcCompetence) + .filter_by(referentiel_id=refcomp.id) + .all() + ) + return { + validation.ue2.niveau_competence.id: validation for validation in validations + } diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py index dff4ead7b3..70e1e8295c 100644 --- a/app/scodoc/codes_cursus.py +++ b/app/scodoc/codes_cursus.py @@ -204,6 +204,7 @@ CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP} CODES_UE_CAPITALISANTS = {ADM} "UE capitalisée" +CODES_JURY_RCUE = CODES_JURY_UE # tous les codes d'UEs sont utilisables pour les RCUEs CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP} CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP} "Niveau RCUE validé" diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index fee4064f4c..f192aa8a1b 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -313,9 +313,26 @@ def ficheEtud(etudid=None): ) info[ "link_bul_pdf" - ] = f"""tous les bulletins""" + ] = f""" + + tous les bulletins + + """ + last_formsemestre: FormSemestre = db.session.get( + FormSemestre, info["sems"][0]["formsemestre_id"] + ) + if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2: + info[ + "link_bul_pdf" + ] += f""" + + Visualiser les compétences BUT + + """ if authuser.has_permission(Permission.ScoEtudInscrit): info[ "link_inscrire_ailleurs" diff --git a/app/static/css/parcour_formation.css b/app/static/css/parcour_formation.css index b15e2f71e8..9df26fe03f 100644 --- a/app/static/css/parcour_formation.css +++ b/app/static/css/parcour_formation.css @@ -144,6 +144,10 @@ div.ue.pair { color: black; } +div.rcue { + grid-column: 1 / span 2; +} + /* ne fonctionne pas option.non_associe { background-color: yellow; @@ -153,4 +157,9 @@ option.non_associe { .links { margin-top: 16px; margin-bottom: 8px; -} \ No newline at end of file +} + +select.validation_rcue { + display: inline-block; + margin-left: 32px; +} diff --git a/app/templates/but/validation_rcues.j2 b/app/templates/but/validation_rcues.j2 new file mode 100644 index 0000000000..5ad9139879 --- /dev/null +++ b/app/templates/but/validation_rcues.j2 @@ -0,0 +1,218 @@ +{% extends "sco_page.j2" %} + +{% block styles %} + {{super()}} + + +{% endblock %} + +{% macro show_ue(niv, sem="pair", sem_idx=0) -%} + {% if niv['niveau'] %} + {# Affiche l'UE et sa décision de jury #} + {% if niv['ue_'+sem] %} + {{ niv['ue_'+sem].acronyme }} + {% set validation = ue_validation_by_niveau.get((niv['niveau'].id, sem)) %} +
+
+ {% if validation %} + {{validation.code}} + {% else %} + - + {% endif %} +
+
+ {% if validation %} +
Validation de {{niv['ue_'+sem].acronyme}}
+
Jury de {{validation.formsemestre.titre_annee() + if validation.formsemestre else "-"}}
+
enregistrée le {{ + validation.event_date.strftime("%d/%m/%Y à %H:%M") + if validation.event_date else "-" + }}
+ {% else %} + pas de décision de jury enregistrée pour cette UE + {% endif %} +
+
+ + {% else %} + {{scu.EMO_WARNING|safe}} non associé + {% endif %} + {% endif %} +{%- endmacro %} + +{% block app_content %} + +{# Résultats dans le parcours #} +
+ +
+ Validations de {{sco.etud.html_link_fiche()|safe}} + {% if parcour %} + parcours {{parcour.code}} « {{parcour.libelle}} » + {% else %} + non inscrit à un parcours de la spécialité + {% endif %} +
+ +{% 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() %} +
+
+ + {% if niv['niveau'].is_tronc_commun %} + TC + {% elif niv['niveau'].parcours|length > 1 %} + + {% set virg = joiner(", ") %} + {% for p in niv['niveau'].parcours %} + {{ virg() }}{{p.code}} + {% endfor %} + + {% endif %} + + {{niv['niveau'].libelle if niv['niveau'] else ''}} +
+
+
+ {% set validation = rcue_validation_by_niveau.get(niv['niveau'].id) %} + {% if validation %} +
+ RCUE enregistré {{validation.code}} + {% if niv['niveau'] and edit %} + {% if not (niv['ue_pair'] and niv['ue_impair']) %} + + {% else %} + + {% endif %} + {% endif %} +
+
+
Validation du RCUE
+
enregistrée le {{ + validation.date.strftime("%d/%m/%Y à %H:%M") + if validation.date else "-" + }} +
+
par le jury de {{validation.formsemestre.titre_annee() + if validation.formsemestre else "-"}} +
+
+ {% endif %} +
+
+
+ {{ show_ue(niv, "impair", 2*annee-1) }} +
+
+ {{ show_ue(niv, "pair", 2*annee) }} +
+
+ {% endfor %} +
+
+{% endfor %} +
+ +{% if sco.formsemestre.can_edit_jury() %} +
+ {% if edit %} + quitter le mode édition des RCUEs + {% else %} + éditer les décisions d'RCUE antérieurs + {% endif %} + +
+{% endif %} + +
+ +

Cette page montre les validations d'UEs et de niveaux de compétences (RCUEs) +de {{sco.etud.html_link_fiche()|safe}} +dans le +{%if parcour %} +parcours {{parcour.code}} +{% else %} +tronc commun +{% endif %} +du référentiel de compétence {{formation.referentiel_competence.specialite}} +

+ +

Seuls les UEs et niveaux de ce référentiel sont montrés. Si le référentiel a +changé, enregistrer des validations "antérieures". +

+ +

Le symbole TC désigne un niveau du tronc commun +(c'est à dire présent dans tous les parcours de la spécialité).

+ +{% if edit %} +

Les validations sont enregistrées au fur et à mesure.

+{% endif %} +
+ + + +{% endblock %} \ No newline at end of file diff --git a/app/views/__init__.py b/app/views/__init__.py index 9d342dbbf4..26a241601f 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -50,7 +50,7 @@ def close_dept_db_connection(arg): class ScoData: """Classe utilisée pour passer des valeurs aux vues (templates)""" - def __init__(self, etud=None, formsemestre=None): + def __init__(self, etud: Identite = None, formsemestre: FormSemestre = None): # Champs utilisés par toutes les pages ScoDoc (sidebar, en-tête) self.Permission = Permission self.scu = scu @@ -96,6 +96,7 @@ class ScoData: self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar( self.sem ) + self.formsemestre = formsemestre # --- Préférences # prefs fallback to global pref if sem is None: if formsemestre: diff --git a/app/views/but_formation.py b/app/views/but_formation.py index d15d90c87c..64e4d90af9 100644 --- a/app/views/but_formation.py +++ b/app/views/but_formation.py @@ -30,7 +30,8 @@ Emmanuel Viennet, 2023 from flask import flash, g, redirect, render_template, request, url_for -from app import db, log +from app import db +from app.but import cursus_but, validations_view from app.decorators import ( scodoc, permission_required, @@ -39,16 +40,16 @@ from app.decorators import ( from app.forms.formation.ue_parcours_ects import UEParcoursECTSForm from app.models import ( - ApcCompetence, - ApcNiveau, ApcParcours, ApcReferentielCompetences, Formation, + FormSemestre, + Identite, UniteEns, ) from app.scodoc.codes_cursus import UE_STANDARD from app.scodoc.sco_permissions import Permission -from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_exceptions import ScoPermissionDenied, ScoValueError from app.views import notes_bp as bp from app.views import ScoData @@ -74,7 +75,9 @@ def parcour_formation(formation_id: int, parcour_id: int = None) -> str: raise ScoValueError("parcours invalide ou hors référentiel de formation") competences_parcour = ( - parcour_formation_competences(parcour, formation) if parcour else None + cursus_but.parcour_formation_competences(parcour, formation) + if parcour + else None ) return render_template( @@ -87,103 +90,32 @@ def parcour_formation(formation_id: int, parcour_id: int = None) -> str: ) -def ue_associee_au_niveau_du_parcours( - ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S" -) -> UniteEns: - "L'UE associée à ce niveau, ou None" - ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id] - if len(ues) > 1: - # plusieurs UEs associées à ce niveau: élimine celles sans parcours - ues_pair_avec_parcours = [ue for ue in ues if ue.parcours] - if ues_pair_avec_parcours: - ues = ues_pair_avec_parcours - if len(ues) > 1: - log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}") - return ues[0] if ues else None - - -def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> list: +@bp.route( + "/validation_rcues///edit", + defaults={"edit": True}, + endpoint="validation_rcues_edit", +) +@bp.route("/validation_rcues//") +@scodoc +@permission_required(Permission.ScoView) +def validation_rcues( + formsemestre_id: int, etudid: int = None, edit: bool = False +) -> str: + """Visualisation des résultats UEs et RCUEs d'un étudiant + et saisie des validation de RCUE antérieures. """ - [ - { - 'competence' : ApcCompetence, - 'niveaux' : { - 1 : { ... }, - 2 : { ... }, - 3 : { - 'niveau' : ApcNiveau, - 'ue_impair' : UniteEns, # actuellement associée - 'ues_impair' : list[UniteEns], # choix possibles - 'ue_pair' : UniteEns, - 'ues_pair' : list[UniteEns], - } - } - } - ] - """ - - def _niveau_ues(competence: ApcCompetence, annee: int) -> dict: - "niveau et ues pour cette compétence de cette année du parcours" - 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} parcours {parcour.code}""" + etud: Identite = Identite.query.get_or_404(etudid) + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + if edit: # check permission + if not formsemestre.can_edit_jury(): + raise ScoPermissionDenied( + dest_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, ) - niveau = niveaux[0] - elif len(niveaux) == 0: - return { - "niveau": None, - "ue_pair": None, - "ue_impair": None, - "ues_pair": [], - "ues_impair": [], - } - # Toutes les UEs de la formation dans ce parcours ou tronc commun - ues = [ - ue - for ue in formation.ues - if ((not ue.parcours) or (parcour.id in (p.id for p in ue.parcours))) - and ue.type == UE_STANDARD - ] - ues_pair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee)] - ues_impair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)] - - # UE associée au niveau dans ce parcours - ue_pair = ue_associee_au_niveau_du_parcours( - ues_pair_possibles, niveau, f"S{2*annee}" - ) - ue_impair = ue_associee_au_niveau_du_parcours( - ues_impair_possibles, niveau, f"S{2*annee-1}" - ) - - return { - "niveau": niveau, - "ue_pair": ue_pair, - "ues_pair": [ - ue - for ue in ues_pair_possibles - if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id - ], - "ue_impair": ue_impair, - "ues_impair": [ - ue - for ue in ues_impair_possibles - if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id - ], - } - - competences = [ - { - "competence": competence, - "niveaux": {annee: _niveau_ues(competence, annee) for annee in (1, 2, 3)}, - } - for competence in parcour.query_competences() - ] - return competences + ) + return validations_view.validation_rcues(etud, formsemestre, edit) @bp.route("/ue_parcours_ects/", methods=["GET", "POST"]) diff --git a/tests/api/test_api_jury.py b/tests/api/test_api_jury.py index 64f59c273b..a0bd8a32a8 100644 --- a/tests/api/test_api_jury.py +++ b/tests/api/test_api_jury.py @@ -16,8 +16,9 @@ Utilisation : Lancer : pytest tests/api/test_api_jury.py """ +import requests - +from app.scodoc import sco_utils as scu from tests.api.setup_test_api import ( API_URL, CHECK_CERTIFICATE, @@ -37,4 +38,29 @@ def test_jury_decisions(api_headers): decisions_jury = GET( f"/formsemestre/{formsemestre_id}/decisions_jury", headers=api_headers ) + assert len(etudiants) > 0 assert len(etudiants) == len(decisions_jury) + # TODO La suite de ce test est a compléter: il faut modifier le formation test RT + # pour avoir au moins le S2 et le S2: actuellement seulement le S1 + # # Récupère la formation de ce semestre pour avoir les UEs + # r = requests.get( + # API_URL + "/formation/1/export", + # headers=api_headers, + # verify=CHECK_CERTIFICATE, + # timeout=scu.SCO_TEST_API_TIMEOUT, + # ) + # assert r.status_code == 200 + # export_formation = r.json() + # ues = export_formation["ue"] + # # Enregistre une validation d'RCUE + # etudid = etudiants[0]["id"] + # validation = POST_JSON( + # f"/etudiant/{etudid}/jury/validation_rcue/record", + # data={ + # "code": "ADM", + # "ue1_id": XXX, + # "ue2_id": XXX, + # }, + # headers=api_headers, + # ) + # assert validation