diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 1d8c08fcec..818809ebb7 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -188,14 +188,21 @@ class DecisionsProposeesAnnee(DecisionsProposees): "le 1er semestre de l'année scolaire considérée (S1, S3, S5)" self.formsemestre_pair = formsemestre_pair "le second formsemestre de la même année scolaire (S2, S4, S6)" - self.annee_but = formsemestre_impair.semestre_id // 2 + 1 + self.annee_but = ( + formsemestre_impair.semestre_id // 2 + 1 + if formsemestre_impair + else formsemestre_pair.semestre_id // 2 + ) "le rang de l'année dans le BUT: 1, 2, 3" assert self.annee_but in (1, 2, 3) - self.validation = ApcValidationAnnee.query.filter_by( - etudid=self.etud.id, - formsemestre_id=formsemestre_impair.id, - ordre=self.annee_but, - ).first() + if self.formsemestre_impair is not None: + self.validation = ApcValidationAnnee.query.filter_by( + etudid=self.etud.id, + formsemestre_id=formsemestre_impair.id, + ordre=self.annee_but, + ).first() + else: + self.validation = None if self.validation is not None: self.code_valide = self.validation.code self.parcour = None diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py new file mode 100644 index 0000000000..0fafa02b53 --- /dev/null +++ b/app/but/jury_but_recap.py @@ -0,0 +1,179 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT: table recap annuelle et liens saisie +""" + +import time +from flask import g, url_for +from app import db + +from app.but import jury_but +from app.comp.res_but import ResultatsSemestreBUT +from app.comp import res_sem +from app.models.etudiants import Identite +from app.models.formsemestre import FormSemestre +from app.models.ues import UniteEns + +from app.scodoc import sco_formsemestre_status +from app.scodoc import html_sco_header +from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoException, ScoValueError + + +class RowCollector: + def __init__(self, cells: dict = None, titles: dict = None): + self.titles = titles + self.row = cells or {} # col_id : str + self.idx = 0 + + def add_cell( + self, + col_id: str, + title: str, + content: str, + classes: str = "", + idx: int = None, + ): + "Add a row to our table. classes is a list of css class names" + self.idx = idx if idx is not None else self.idx + self.row[col_id] = content + if classes: + self.row[f"_{col_id}_class"] = classes + f" c{self.idx}" + if not col_id in self.titles: + self.titles[col_id] = title + self.titles[f"_{col_id}_col_order"] = self.idx + if classes: + self.titles[f"_{col_id}_class"] = classes + self.idx += 1 + + def __setitem__(self, key, value): + self.row[key] = value + + def __getitem__(self, key): + return self.row[key] + + +def formsemestre_saisie_jury_but( + formsemestre2: FormSemestre, readonly: bool = False +) -> str: + """formsemestre est un semestre PAIR + Si readonly, ne montre pas le lien "saisir la décision" + + => page html complète + """ + # Quick & Dirty + # pour chaque etud de res2 trié + # S1: UE1, ..., UEn + # S2: UE1, ..., UEn + # + # UE1_s1, UE1_s2, moy_rcue, UE2... , Nbrcue_validables, Nbrcue<8, passage_de_droit, valide_moitie_rcue + # + # Pour chaque etud de res2 trié + # DecisionsProposeesAnnee(etud, formsemestre2) + # Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur + # -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc + if formsemestre2.semestre_id % 2 != 0: + raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs") + + rows, titles, column_ids = get_table_jury_but(formsemestre2, readonly=readonly) + if not rows: + return ( + '
aucun étudiant !
' + ) + filename = scu.sanitize_filename( + f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}""" + ) + table_html = build_table_jury_but_html(filename, rows, titles, column_ids) + H = [ + html_sco_header.sco_header( + page_title=f"{formsemestre2.sem_modalite()}: moyennes", + no_side_bar=True, + init_qtip=True, + javascripts=["js/etud_info.js", "js/table_recap.js"], + ), + sco_formsemestre_status.formsemestre_status_head( + formsemestre_id=formsemestre2.id + ), + ] + H.append(table_html) + H.append(html_sco_header.sco_footer()) + return "\n".join(H) + + +def build_table_jury_but_html(filename: str, rows, titles, column_ids) -> str: + """assemble la table html""" + footer_rows = [] # inutile pour l'instant, à voir XXX + selected_etudid = None # inutile pour l'instant, à voir XXX + H = [ + f"""
""" + ] + # header + H.append( + f""" + + {scu.gen_row(column_ids, titles, "th")} + + """ + ) + # body + H.append("") + for row in rows: + H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") + H.append("\n") + # footer + H.append("") + idx_last = len(footer_rows) - 1 + for i, row in enumerate(footer_rows): + H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n') + H.append( + """ + +
+
+ """ + ) + return "".join(H) + + +def get_table_jury_but( + formsemestre2: FormSemestre, readonly: bool = False +) -> tuple[list[dict], list[str], list[str]]: + """Construit la table des résultats annuels pour le jury BUT""" + res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2) + rcues = [] + + titles = {} # column_id : title + rows = [] + for etudid in formsemestre2.etuds_inscriptions: + etud = Identite.query.get(etudid) + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2) + row = RowCollector(titles=titles) + # --- Codes (seront cachés, mais exportés en excel) + row.add_cell("etudid", "etudid", etudid, "codes") + row.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes") + # --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser) + row.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") + row.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail") + row["_nom_disp_order"] = etud.sort_key + row.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") + row.add_cell("nom_short", "Nom", etud.nom_short, "identite_court") + row["_nom_short_order"] = etud.sort_key + row["_nom_short_target"] = url_for( + "notes.formsemestre_bulletinetud", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre2.id, + etudid=etudid, + ) + row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"' + row["_nom_disp_target"] = row["_nom_short_target"] + row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"] + + rows.append(row.row) + column_ids = [title for title in titles if not title.startswith("_")] + column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000)) + return rows, titles, column_ids diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index e06394e526..aea7eb0a76 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -410,10 +410,9 @@ def formsemestre_status_menubar(sem): }, { "title": "Saisie des décisions du jury", - "endpoint": "notes.formsemestre_recapcomplet", + "endpoint": "notes.formsemestre_saisie_jury", "args": { "formsemestre_id": formsemestre_id, - "modejury": 1, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), }, diff --git a/app/views/notes.py b/app/views/notes.py index ddb83f56b8..d7f4cbd854 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -40,7 +40,6 @@ import flask from flask import abort, flash, jsonify, redirect, render_template, url_for from flask import current_app, g, request from flask_login import current_user -from werkzeug.utils import redirect from app.but import jury_but from app.comp import res_sem @@ -57,7 +56,7 @@ from app import db from app import models from app.models import ScolarNews from app.auth.models import User -from app.but import apc_edit_ue, bulletin_but +from app.but import apc_edit_ue, bulletin_but, jury_but_recap from app.decorators import ( scodoc, scodoc7func, @@ -2535,6 +2534,31 @@ def formsemestre_validation_suppress_etud( # ------------- PV de JURY et archives sco_publish("/formsemestre_pvjury", sco_pvjury.formsemestre_pvjury, Permission.ScoView) + + +@bp.route("/formsemestre_saisie_jury") +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def formsemestre_saisie_jury(formsemestre_id: int): + """Page de saisie: liste des étudiants et lien vers page jury + en semestres pairs de BUT, table spécifique avec l'année + sinon, redirect vers page recap en mode jury + """ + readonly = not sco_permissions_check.can_validate_sem(formsemestre_id) + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0: + return jury_but_recap.formsemestre_saisie_jury_but(formsemestre, readonly) + return redirect( + url_for( + "notes.formsemestre_recapcomplet", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + modejury=1, + ) + ) + + sco_publish( "/formsemestre_lettres_individuelles", sco_pvjury.formsemestre_lettres_individuelles,