############################################################################## # # ScoDoc # # Copyright (c) 1999 - 2024 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 jurys et validations Emmanuel Viennet, 2024 """ from collections import defaultdict import datetime import flask from flask import flash, g, redirect, render_template, request, url_for from flask_login import current_user from app import log from app.but import ( cursus_but, jury_edit_manual, jury_but, jury_but_validation_auto, jury_but_view, ) from app.but.forms import jury_but_forms from app.comp import jury from app.decorators import ( scodoc, scodoc7func, permission_required, ) from app.models import ( Evaluation, Formation, FormSemestre, FormSemestreInscription, Identite, ScolarAutorisationInscription, ScolarFormSemestreValidation, ScolarNews, ScoDocSiteConfig, ) from app.scodoc import ( html_sco_header, sco_bulletins_json, sco_cache, sco_formsemestre_exterieurs, sco_formsemestre_validation, sco_preferences, ) from app.scodoc.codes_cursus import CODES_UE_VALIDES from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ( ScoPermissionDenied, ScoValueError, ) from app.scodoc.sco_permissions import Permission from app.scodoc.sco_pv_dict import descr_autorisations # from app.scodoc.TrivialFormulator import TrivialFormulator from app.views import notes_bp as bp from app.views import ScoData # --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES @bp.route("/formsemestre_validation_etud_form") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_validation_etud_form( formsemestre_id, etudid=None, etud_index=None, check=0, desturl="", sortcol=None, ): "Formulaire choix jury pour un étudiant" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) read_only = not formsemestre.can_edit_jury() if formsemestre.formation.is_apc(): return redirect( url_for( "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid=etudid, ) ) return sco_formsemestre_validation.formsemestre_validation_etud_form( formsemestre_id, etudid=etudid, etud_index=etud_index, check=check, read_only=read_only, dest_url=desturl, sortcol=sortcol, ) @bp.route("/formsemestre_validation_etud") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_validation_etud( formsemestre_id, etudid=None, codechoice=None, desturl="", sortcol=None, ): "Enregistre choix jury pour un étudiant" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) return sco_formsemestre_validation.formsemestre_validation_etud( formsemestre_id, etudid=etudid, codechoice=codechoice, desturl=desturl, sortcol=sortcol, ) @bp.route("/formsemestre_validation_etud_manu") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_validation_etud_manu( formsemestre_id, etudid=None, code_etat="", new_code_prev="", devenir="", assidu=False, desturl="", sortcol=None, ): "Enregistre choix jury pour un étudiant" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) return sco_formsemestre_validation.formsemestre_validation_etud_manu( formsemestre_id, etudid=etudid, code_etat=code_etat, new_code_prev=new_code_prev, devenir=devenir, assidu=assidu, desturl=desturl, sortcol=sortcol, ) # --- Jurys BUT @bp.route( "/formsemestre_validation_but//", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.ScoView) def formsemestre_validation_but( formsemestre_id: int, etudid: int, ): "Form. saisie décision jury semestre BUT" formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() # la route ne donne pas le type d'etudid pour pouvoir construire des URLs # provisoires avec NEXT et PREV try: etudid = int(etudid) except ValueError as exc: raise ScoValueError("adresse invalide") from exc etud = Identite.get_etud(etudid) nb_etuds = formsemestre.etuds.count() read_only = not formsemestre.can_edit_jury() can_erase = current_user.has_permission(Permission.EtudInscrit) # --- Navigation prev_lnk = ( f"""{scu.EMO_PREV_ARROW} précédent """ if nb_etuds > 1 else "" ) next_lnk = ( f"""suivant {scu.EMO_NEXT_ARROW} """ if nb_etuds > 1 else "" ) navigation_div = f"""
""" H = [ html_sco_header.sco_header( page_title=f"Validation BUT S{formsemestre.semestre_id}", formsemestre_id=formsemestre_id, etudid=etudid, cssstyles=[ "css/jury_but.css", "css/cursus_but.css", ], javascripts=("js/jury_but.js",), ), """
""", ] if formsemestre.etuds_inscriptions[etudid].etat != scu.INSCRIT: return ( "\n".join(H) + f"""
Jury BUT
{etud.html_link_fiche()}
Impossible de statuer sur cet étudiant: il est démissionnaire ou défaillant (voir sa fiche)
{navigation_div}
""" + html_sco_header.sco_footer() ) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) has_notes_en_attente = deca.has_notes_en_attente() evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud( formsemestre, etud ) if has_notes_en_attente or evaluations_a_debloquer: read_only = True if request.method == "POST": if not read_only: deca.record_form(request.form) ScolarNews.add( typ=ScolarNews.NEWS_JURY, obj=formsemestre.id, text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""", url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ), ) flash("codes enregistrés") return flask.redirect( url_for( "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid=etudid, ) ) warning = "" if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau): warning += f"""
Attention: {len(deca.niveaux_competences)} niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.
""" if (deca.parcour is None) and len(formsemestre.parcours) > 0: warning += ( """
L'étudiant n'est pas inscrit à un parcours.
""" ) if formsemestre.date_fin - datetime.date.today() > datetime.timedelta(days=12): # encore loin de la fin du semestre de départ de ce jury ? warning += f"""
Le semestre S{formsemestre.semestre_id} terminera le {formsemestre.date_fin.strftime(scu.DATE_FMT)} : êtes-vous certain de vouloir enregistrer une décision de jury ?
""" if deca.formsemestre_impair: inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id) if (not inscription) or inscription.etat != scu.INSCRIT: etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") warning += f"""
{etat_ins} en S{deca.formsemestre_impair.semestre_id}
""" if deca.formsemestre_pair: inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id) if (not inscription) or inscription.etat != scu.INSCRIT: etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") warning += f"""
{etat_ins} en S{deca.formsemestre_pair.semestre_id}
""" if has_notes_en_attente: warning += f"""
{etud.html_link_fiche() } a des notes en ATTente dans les modules suivants. Vous devez régler cela avant de statuer en jury !
" if evaluations_a_debloquer: links_evals = [ f"""{e.description} en {e.moduleimpl.module.code}""" for e in evaluations_a_debloquer ] warning += f"""
Impossible de statuer sur cet étudiant: il a des notes dans des évaluations qui seront débloquées plus tard: voir {", ".join(links_evals)} """ if warning: warning = f"""
{warning}
""" H.append( f"""
Jury BUT{deca.annee_but} - Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"} - {deca.annee_scolaire_str()}
{etud.html_link_fiche()}
{warning}
""" ) H.append(jury_but_view.show_etud(deca, read_only=read_only)) autorisations_idx = deca.get_autorisations_passage() div_autorisations_passage = ( f"""
Autorisé à passer en : { ", ".join( ["S" + str(i) for i in autorisations_idx ] )}
""" if autorisations_idx else """
pas d'autorisations de passage enregistrées.
""" ) H.append(div_autorisations_passage) if read_only: H.append( f"""
{"Vous n'avez pas la permission de modifier ces décisions." if formsemestre.etat else "Semestre verrouillé."} Les champs entourés en vert sont enregistrés.
""" ) else: erase_span = f""" effacer des décisions de jury enregistrer des UEs antérieures décerner le DUT "120ECTS" """ H.append( f"""
permettre la saisie manuelles des codes {"d'année et " if deca.jury_annuel else ""} de niveaux. Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année !
{erase_span}
""" ) H.append(navigation_div) H.append("
") # Affichage cursus BUT but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation) H += [ """
Niveaux de compétences enregistrés :
""", render_template( "but/cursus_etud.j2", cursus=but_cursus, scu=scu, ), "
", ] H.append( render_template( "but/documentation_codes_jury.j2", nom_univ=f"""Export {sco_preferences.get_preference("InstituteName") or sco_preferences.get_preference("UnivName") or "Apogée"}""", codes=ScoDocSiteConfig.get_codes_apo_dict(), ) ) H.append( f"""
{scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE capitalisées (note > 10) lors d'une année précédente peuvent être prise en compte pour former un RCUE (associé à un niveau de compétence du BUT).
""" ) return "\n".join(H) + html_sco_header.sco_footer() @bp.route( "/formsemestre_validation_auto_but/", methods=["GET", "POST"] ) @scodoc @permission_required(Permission.ScoView) def formsemestre_validation_auto_but(formsemestre_id: int = None): "Saisie automatique des décisions de jury BUT" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) if not formsemestre.formation.is_apc(): raise ScoValueError( "formsemestre_validation_auto_but est réservé aux formations APC" ) formsemestre = FormSemestre.get_formsemestre(formsemestre_id) form = jury_but_forms.FormSemestreValidationAutoBUTForm() if request.method == "POST": if not form.cancel.data: nb_etud_modif, _ = ( jury_but_validation_auto.formsemestre_validation_auto_but(formsemestre) ) flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)") return redirect( url_for( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1, ) ) # Avertissement si formsemestre impair formsemestres_suspects = {} if formsemestre.semestre_id % 2: _, decas = jury_but_validation_auto.formsemestre_validation_auto_but( formsemestre, dry_run=True ) # regarde si il y a des semestres pairs postérieurs qui ne soient pas bloqués formsemestres_suspects = { deca.formsemestre_pair.id: deca.formsemestre_pair for deca in decas if deca.formsemestre_pair and deca.formsemestre_pair.date_debut > formsemestre.date_debut and not deca.formsemestre_pair.block_moyennes } return render_template( "but/formsemestre_validation_auto_but.j2", form=form, formsemestres_suspects=formsemestres_suspects, sco=ScoData(formsemestre=formsemestre), title="Calcul automatique jury BUT", ) @bp.route( "/formsemestre_validate_previous_ue//", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.ScoView) def formsemestre_validate_previous_ue(formsemestre_id, etudid=None): "Form. saisie UE validée hors ScoDoc" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) etud: Identite = ( Identite.query.filter_by(id=etudid) .join(FormSemestreInscription) .filter_by(formsemestre_id=formsemestre_id) .first_or_404() ) return sco_formsemestre_validation.formsemestre_validate_previous_ue( formsemestre, etud ) @bp.route("/formsemestre_ext_edit_ue_validations", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid=None): "Form. edition UE semestre extérieur" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations( formsemestre_id, etudid ) @bp.route("/formsemestre_validation_auto") @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_validation_auto(formsemestre_id): "Formulaire saisie automatisee des decisions d'un semestre" formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) if formsemestre.formation.is_apc(): return redirect( url_for( "notes.formsemestre_validation_auto_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ) ) return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id) @bp.route("/do_formsemestre_validation_auto") @scodoc @permission_required(Permission.ScoView) @scodoc7func def do_formsemestre_validation_auto(formsemestre_id): "Formulaire saisie automatisee des decisions d'un semestre" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) return sco_formsemestre_validation.do_formsemestre_validation_auto(formsemestre_id) @bp.route("/formsemestre_validation_suppress_etud", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) @scodoc7func def formsemestre_validation_suppress_etud( formsemestre_id, etudid, dialog_confirmed=False ): """Suppression des décisions de jury pour un étudiant.""" formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) etud = Identite.get_etud(etudid) if formsemestre.formation.is_apc(): next_url = url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, ) else: next_url = url_for( "notes.formsemestre_validation_etud_form", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid=etudid, ) if not dialog_confirmed: d = sco_bulletins_json.dict_decision_jury( etud, formsemestre, with_decisions=True ) descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])] dec_annee = d.get("decision_annee") if dec_annee: descr_annee = dec_annee.get("code", "-") else: descr_annee = "-" existing = f"""
  • Semestre : {d.get("decision", {"code":"-"})['code'] or "-"}
  • Année BUT: {descr_annee}
  • UEs : {", ".join(descr_ues)}
  • RCUEs: {len(d.get("decision_rcue", []))} décisions
  • Autorisations: {descr_autorisations(ScolarAutorisationInscription.query.filter_by(origin_formsemestre_id=formsemestre_id, etudid=etudid))}
""" return scu.confirm_dialog( f"""

Confirmer la suppression des décisions du semestre {formsemestre.titre_mois()} pour {etud.nomprenom}

Cette opération est irréversible.

{existing}
""", OK="Supprimer", dest_url="", cancel_url=next_url, parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, ) sco_formsemestre_validation.formsemestre_validation_suppress_etud( formsemestre_id, etudid ) flash("Décisions supprimées") return flask.redirect(next_url) @bp.route( "/formsemestre_jury_erase/", methods=["GET", "POST"], defaults={"etudid": None}, ) @bp.route( "/formsemestre_jury_erase//", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.ScoView) def formsemestre_jury_erase(formsemestre_id: int, etudid: int = None): """Supprime toutes les décisions de jury (classique ou BUT) pour cette année. Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits. En BUT, si only_one_sem n'efface que pour le formsemestre indiqué, pas les deux de l'année. En classique, n'affecte que les décisions issues de ce formsemestre. """ only_one_sem = int(request.args.get("only_one_sem") or False) formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) is_apc = formsemestre.formation.is_apc() if etudid is None: etud = None etuds = formsemestre.get_inscrits(include_demdef=True) dest_url = url_for( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1, ) else: etud = Identite.get_etud(etudid) etuds = [etud] endpoint = ( "notes.formsemestre_validation_but" if is_apc else "notes.formsemestre_validation_etud_form" ) dest_url = url_for( endpoint, scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid=etudid, ) if request.method == "POST": with sco_cache.DeferredSemCacheManager(): for etud in etuds: if is_apc: deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca.erase(only_one_sem=only_one_sem) else: sco_formsemestre_validation.formsemestre_validation_suppress_etud( formsemestre.id, etud.id ) log(f"formsemestre_jury_erase({formsemestre_id}, {etud.id})") flash( ( "décisions de jury du semestre effacées" if (only_one_sem or is_apc) else "décisions de jury des semestres de l'année BUT effacées" ) + f" pour {len(etuds)} étudiant{'s' if len(etuds) > 1 else ''}" ) return redirect(dest_url) return render_template( "confirm_dialog.j2", title=f"""Effacer les validations de jury { ("de " + etud.nomprenom) if etud else ("des " + str(len(etuds)) + " étudiants inscrits dans ce semestre") } ?""", explanation=( ( f"""Les validations d'UE et autorisations de passage du semestre S{formsemestre.semestre_id} seront effacées.""" if (only_one_sem or is_apc) else """Les validations de toutes les UE, RCUE (compétences) et année issues de cette année scolaire seront effacées. """ ) + """

Les décisions des années scolaires précédentes ne seront pas modifiées.

""" + """

Efface aussi toutes les validations concernant l'année BUT de ce semestre, même si elles ont été acquises ailleurs, ainsi que les validations de DUT en 120 ECTS obtenues après BUT1/BUT2.

""" if is_apc else "" + """
Cette opération est irréversible ! A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite.
""" ), cancel_url=dest_url, ) @bp.route( "/erase_decisions_annee_formation///", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.EtudInscrit) def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int): """Efface toute les décisions d'une année pour cet étudiant""" etud: Identite = Identite.query.get_or_404(etudid) formation: Formation = Formation.query.filter_by( id=formation_id, dept_id=g.scodoc_dept_id ).first_or_404() if request.method == "POST": jury.erase_decisions_annee_formation(etud, formation, annee, delete=True) flash("Décisions de jury effacées") return redirect( url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id, ) ) validations = jury.erase_decisions_annee_formation(etud, formation, annee) formsemestre_origine_id = request.args.get("formsemestre_id") formsemestre_origine = ( FormSemestre.query.get_or_404(formsemestre_origine_id) if formsemestre_origine_id else None ) return render_template( "jury/erase_decisions_annee_formation.j2", annee=annee, cancel_url=url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id ), etud=etud, formation=formation, formsemestre_origine=formsemestre_origine, validations=validations, sco=ScoData(), title=f"Effacer décisions de jury {etud.nom} - année {annee}", ) @bp.route( "/jury_delete_manual/", methods=["GET", "POST"], ) @scodoc @permission_required(Permission.EtudInscrit) def jury_delete_manual(etudid: int): """Efface toute les décisions d'une année pour cet étudiant""" etud = Identite.get_etud(etudid) return jury_edit_manual.jury_delete_manual(etud) @bp.route("/etud_bilan_ects/") @scodoc @permission_required(Permission.ScoView) def etud_bilan_ects(etudid: int): """Page bilan de tous els ECTS acquis par un étudiant. Plusieurs formations (eg DUT, LP) peuvent être concernées. """ etud = Identite.get_etud(etudid) # Cherche les formations différentes (au sens des ECTS) # suivies par l'étudiant: regroupe ses formsemestres # diplome est la clé: en classique le code formation, en BUT le referentiel_competence_id formsemestre_by_diplome = defaultdict(list) for formsemestre in etud.get_formsemestres(recent_first=True): diplome = ( formsemestre.formation.referentiel_competence.id if ( formsemestre.formation.is_apc() and formsemestre.formation.referentiel_competence ) else formsemestre.formation.formation_code ) formsemestre_by_diplome[diplome].append(formsemestre) # Pour chaque liste de formsemestres d'un même "diplôme" # liste les UE validées avec leurs ECTS ects_by_diplome = {} titre_by_diplome = {} # { diplome : titre } validations_by_diplome = {} # { diplome : query validations UEs } for diplome, formsemestres in formsemestre_by_diplome.items(): formsemestre = formsemestres[0] titre_by_diplome[diplome] = formsemestre.formation.get_titre_version() if formsemestre.formation.is_apc(): validations = cursus_but.but_validations_ues(etud, diplome) else: validations = ScolarFormSemestreValidation.validations_ues( etud, formsemestre.formation.formation_code ) validations_by_diplome[diplome] = [ validation for validation in validations if validation.code in CODES_UE_VALIDES ] ects_by_diplome[diplome] = sum( (validation.ue.ects or 0.0) for validation in validations_by_diplome[diplome] ) return render_template( "jury/etud_bilan_ects.j2", etud=etud, ects_by_diplome=ects_by_diplome, formsemestre_by_diplome=formsemestre_by_diplome, titre_by_diplome=titre_by_diplome, validations_by_diplome=validations_by_diplome, )