diff --git a/app/api/jury.py b/app/api/jury.py index f31d1a78808f7b281e7c3badad4ee16f44de732d..383396e58458e45d6e148dcef58538086333279f 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -5,7 +5,7 @@ ############################################################################## """ - ScoDoc 9 API : jury + ScoDoc 9 API : jury WIP """ from flask import g, jsonify, request @@ -17,8 +17,8 @@ from app.api import api_bp as bp, api_web_bp from app.decorators import scodoc, permission_required from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_utils import json_error -from app.but import jury_but_recap -from app.models import FormSemestre, FormSemestreInscription, Identite +from app.but import jury_but_results +from app.models import FormSemestre from app.scodoc.sco_permissions import Permission @@ -33,7 +33,7 @@ def decisions_jury(formsemestre_id: int): formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) if formsemestre.formation.is_apc(): app.set_sco_dept(formsemestre.departement.acronym) - rows = jury_but_recap.get_jury_but_results(formsemestre) + rows = jury_but_results.get_jury_but_results(formsemestre) return jsonify(rows) else: raise ScoException("non implemente") diff --git a/app/but/jury_but_results.py b/app/but/jury_but_results.py new file mode 100644 index 0000000000000000000000000000000000000000..79aa14df3349b9050839cd0596292d0beffef844 --- /dev/null +++ b/app/but/jury_but_results.py @@ -0,0 +1,94 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT et classiques: récupération des résults pour API +""" + +import numpy as np + +from app.but import jury_but +from app.models.etudiants import Identite +from app.models.formsemestre import FormSemestre +from app.scodoc import sco_pvjury + + +def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]: + """Liste des résultats jury BUT sous forme de dict, pour API""" + if formsemestre.formation.referentiel_competence is None: + # pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception) + return [] + dpv = sco_pvjury.dict_pvjury(formsemestre.id) + rows = [] + for etudid in formsemestre.etuds_inscriptions: + rows.append(_get_jury_but_etud_result(formsemestre, dpv, etudid)) + return rows + + +def _get_jury_but_etud_result( + formsemestre: FormSemestre, dpv: dict, etudid: int +) -> dict: + """Résultats de jury d'un étudiant sur un semestre pair de BUT""" + etud: Identite = Identite.query.get(etudid) + dec_etud = dpv["decisions_dict"][etudid] + if formsemestre.formation.is_apc(): + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) + else: + deca = None + row = { + "etudid": etud.id, + "code_nip": etud.code_nip, + "code_ine": etud.code_ine, + "is_apc": dpv["is_apc"], # BUT ou classic ? + "etat": dec_etud["etat"], # I ou D ou DEF + "nb_competences": deca.nb_competences if deca else 0, + } + # --- Les RCUEs + rcue_list = [] + if deca: + for rcue in deca.rcues_annee: + dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) + if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau + dec_ue1 = deca.decisions_ues[rcue.ue_1.id] + dec_ue2 = deca.decisions_ues[rcue.ue_2.id] + rcue_dict = { + "ue_1": { + "ue_id": rcue.ue_1.id, + "moy": None + if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue)) + else dec_ue1.moy_ue, + "code": dec_ue1.code_valide, + }, + "ue_2": { + "ue_id": rcue.ue_2.id, + "moy": None + if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue)) + else dec_ue2.moy_ue, + "code": dec_ue2.code_valide, + }, + "moy": rcue.moy_rcue, + "code": dec_rcue.code_valide, + } + rcue_list.append(rcue_dict) + row["rcues"] = rcue_list + # --- Les UEs + ue_list = [] + if dec_etud["decisions_ue"]: + for ue_id, ue_dec in dec_etud["decisions_ue"].items(): + ue_dict = { + "ue_id": ue_id, + "code": ue_dec["code"], + "ects": ue_dec["ects"], + } + ue_list.append(ue_dict) + row["ues"] = ue_list + # --- Le semestre (pour les formations classiques) + if dec_etud["decision_sem"]: + row["semestre"] = {"code": dec_etud["decision_sem"].get("code")} + else: + row["semestre"] = {} # APC, ... + # --- Autorisations + row["autorisations"] = dec_etud["autorisations"] + return row diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index e323348dedc0650997e7cc9bb518174a23432d8a..416c985ba44089c4e1eebe0304cf677b2a4329a5 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -522,9 +522,9 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True): formsemestre = FormSemestre.query.get_or_404(formsemestre_id) is_apc = formsemestre.formation.is_apc() if format == "html" and is_apc and formsemestre.semestre_id % 2 == 0: - from app.but import jury_but_recap + from app.tables import jury_recap - return jury_but_recap.formsemestre_saisie_jury_but( + return jury_recap.formsemestre_saisie_jury_but( formsemestre, read_only=True, mode="recap" ) # /XXX diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 7343ceb2e8ee45668e3efb673519f4c482452f64..4c1a634013fb8e9bd5f441e32520800230e9ee82 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -54,7 +54,7 @@ from app.scodoc import sco_formsemestre_status from app.scodoc import sco_permissions_check from app.scodoc import sco_preferences from app.tables.recap import TableRecap -from app.but.jury_but_recap import TableJury +from app.tables.jury_recap import TableJury def formsemestre_recapcomplet( diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 9597f9dbb4b0b3f11e543d6c6423efe3366f72e7..0e2f9f109ef2f7b159240bdc78a1f410ceaa244d 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -1,6 +1,8 @@ // Tableau recap notes $(function () { $(function () { + if ($('table.table_recap').length == 0) { return; } + let hidden_colums = [ "etud_codes", "identite_detail", "partition_aux", "partition_rangs", "admission", diff --git a/app/but/jury_but_recap.py b/app/tables/jury_recap.py similarity index 59% rename from app/but/jury_but_recap.py rename to app/tables/jury_recap.py index 6ac6ba0a919d539fd25a6b7e34299caea4a4627b..ce96200262f85c928b454154835f541aca0c393c 100644 --- a/app/but/jury_but_recap.py +++ b/app/tables/jury_recap.py @@ -230,6 +230,19 @@ class RowJury(RowRecap): column_classes={"col_rcue"}, ) + # # --- Les ECTS validés + # ects_valides = 0.0 + # if deca.res_impair: + # ects_valides += deca.res_impair.get_etud_ects_valides(etudid) + # if deca.res_pair: + # ects_valides += deca.res_pair.get_etud_ects_valides(etudid) + # row.add_cell( + # "ects_annee", + # "ECTS", + # f"""{int(ects_valides)}""", + # "col_code_annee", + # ) + def formsemestre_saisie_jury_but( formsemestre: FormSemestre, @@ -316,7 +329,7 @@ def formsemestre_saisie_jury_but( <div class="table_recap"> {table_html} </div> - + <div class="table_jury_but_links"> """ ) @@ -375,263 +388,3 @@ def formsemestre_saisie_jury_but( """ ) return "\n".join(H) - - -def build_table_jury_but_html( - filename: str, rows, titles, column_ids, selected_etudid: int = None, klass="" -) -> str: - """assemble la table html""" - footer_rows = [] # inutilisé pour l'instant - H = [ - f"""<div class="table_recap"><table class="table_recap apc jury table_jury_but {klass}" - data-filename="{filename}">""" - ] - # header - H.append( - f""" - <thead> - {scu.gen_row(column_ids, titles, "th")} - </thead> - """ - ) - # body - H.append("<tbody>") - for row in rows: - H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") - H.append("</tbody>\n") - # footer - H.append("<tfoot>") - 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( - """ - </tfoot> - </table> - </div> - """ - ) - return "".join(H) - - -class RowCollector: - """Une ligne de la table""" - - def __init__( - self, - cells: dict = None, - titles: dict = None, - convert_values=True, - column_classes: dict = None, - ): - self.titles = titles - self.row = cells or {} # col_id : str - self.column_classes = column_classes # col_id : str, css class - self.idx = 0 - self.last_etud_cell_idx = 0 - if convert_values: - self.fmt_note = scu.fmt_note - else: - self.fmt_note = lambda x: x - - def __setitem__(self, key, value): - self.row[key] = value - - def __getitem__(self, key): - return self.row[key] - - def get_row_dict(self): - "La ligne, comme un dict" - # create empty cells - for col_id in self.titles: - if col_id not in self.row: - self.row[col_id] = "" - klass = self.column_classes.get(col_id) - if klass: - self.row[f"_{col_id}_class"] = klass - return self.row - - def add_cell( - self, - col_id: str, - title: str, - content: str, - classes: str = "", - idx: int = None, - column_class="", - ): - """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.column_classes[col_id] = column_class - self.idx += 1 - - -def get_jury_but_table( # XXX A SUPPRIMER apres avoir recupéré les stats - formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True -) -> tuple[list[dict], list[str], list[str], dict]: - """Construit la table des résultats annuels pour le jury BUT - => rows_dict, titles, column_ids, jury_stats - où jury_stats est un dict donnant des comptages sur le jury. - """ - - # /////// XXX /////// XXX ////// - titles = {} # column_id : title - jury_stats = { - "nb_etuds": len(formsemestre2.etuds_inscriptions), - "codes_annuels": collections.Counter(), - } - table = TableJury(res2, mode_jury=True) - for etudid in formsemestre2.etuds_inscriptions: - etud: Identite = Identite.query.get(etudid) - deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2) - # XXX row = RowCollector(titles=titles, column_classes=column_classes) - row = RowJury(table, etudid) - table.add_row(row) - row.add_etud(etud) - # --- Nombre de niveaux - row.add_nb_rcues_cell(deca) - # --- Les RCUEs - for rcue in deca.rcues_annee: - dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) - if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau - row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id]) - row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id]) - row.add_rcue_cells(dec_rcue) - # --- Les ECTS validés - ects_valides = 0.0 - if deca.res_impair: - ects_valides += deca.res_impair.get_etud_ects_valides(etudid) - if deca.res_pair: - ects_valides += deca.res_pair.get_etud_ects_valides(etudid) - row.add_cell( - "ects_annee", - "ECTS", - f"""{int(ects_valides)}""", - "col_code_annee", - ) - # --- Le code annuel existant - row.add_cell( - "code_annee", - "Année", - f"""{deca.code_valide or ''}""", - "col_code_annee", - ) - if deca.code_valide: - jury_stats["codes_annuels"][deca.code_valide] += 1 - # --- Le lien de saisie - if mode != "recap" and with_links: - row.add_cell( - "lien_saisie", - "", - f""" - <a href="{url_for( - 'notes.formsemestre_validation_but', - scodoc_dept=g.scodoc_dept, - etudid=etud.id, - formsemestre_id=formsemestre2.id, - )}" class="stdlink"> - {"voir" if read_only else ("modif." if deca.code_valide else "saisie")} - décision</a> - """ - if deca.inscription_etat == scu.INSCRIT - else deca.inscription_etat, - "col_lien_saisie_but", - ) - rows.append(row) - rows_dict = [row.get_row_dict() for row in rows] - if len(rows_dict) > 0: - col_idx = res2.recap_add_partitions( - rows_dict, titles, col_idx=row.last_etud_cell_idx + 1 - ) - res2.recap_add_cursus(rows_dict, titles, col_idx=col_idx + 1) - 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)) - rows_dict.sort(key=lambda row: row["_nom_disp_order"]) - return rows_dict, titles, column_ids, jury_stats - - -def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]: - """Liste des résultats jury BUT sous forme de dict, pour API""" - if formsemestre.formation.referentiel_competence is None: - # pas de ref. comp., donc pas de decisions de jury (ne lance pas d'exception) - return [] - dpv = sco_pvjury.dict_pvjury(formsemestre.id) - rows = [] - for etudid in formsemestre.etuds_inscriptions: - rows.append(get_jury_but_etud_result(formsemestre, dpv, etudid)) - return rows - - -def get_jury_but_etud_result( - formsemestre: FormSemestre, dpv: dict, etudid: int -) -> dict: - """Résultats de jury d'un étudiant sur un semestre pair de BUT""" - etud: Identite = Identite.query.get(etudid) - dec_etud = dpv["decisions_dict"][etudid] - if formsemestre.formation.is_apc(): - deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) - else: - deca = None - row = { - "etudid": etud.id, - "code_nip": etud.code_nip, - "code_ine": etud.code_ine, - "is_apc": dpv["is_apc"], # BUT ou classic ? - "etat": dec_etud["etat"], # I ou D ou DEF - "nb_competences": deca.nb_competences if deca else 0, - } - # --- Les RCUEs - rcue_list = [] - if deca: - for rcue in deca.rcues_annee: - dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) - if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau - dec_ue1 = deca.decisions_ues[rcue.ue_1.id] - dec_ue2 = deca.decisions_ues[rcue.ue_2.id] - rcue_dict = { - "ue_1": { - "ue_id": rcue.ue_1.id, - "moy": None - if (dec_ue1.moy_ue is None or np.isnan(dec_ue1.moy_ue)) - else dec_ue1.moy_ue, - "code": dec_ue1.code_valide, - }, - "ue_2": { - "ue_id": rcue.ue_2.id, - "moy": None - if (dec_ue2.moy_ue is None or np.isnan(dec_ue2.moy_ue)) - else dec_ue2.moy_ue, - "code": dec_ue2.code_valide, - }, - "moy": rcue.moy_rcue, - "code": dec_rcue.code_valide, - } - rcue_list.append(rcue_dict) - row["rcues"] = rcue_list - # --- Les UEs - ue_list = [] - if dec_etud["decisions_ue"]: - for ue_id, ue_dec in dec_etud["decisions_ue"].items(): - ue_dict = { - "ue_id": ue_id, - "code": ue_dec["code"], - "ects": ue_dec["ects"], - } - ue_list.append(ue_dict) - row["ues"] = ue_list - # --- Le semestre (pour les formations classiques) - if dec_etud["decision_sem"]: - row["semestre"] = {"code": dec_etud["decision_sem"].get("code")} - else: - row["semestre"] = {} # APC, ... - # --- Autorisations - row["autorisations"] = dec_etud["autorisations"] - return row diff --git a/app/tables/recap.py b/app/tables/recap.py index 51e3ba429cb4996f5b572f1ba02caf294a9e08be..7431523c3f4aaf98ab4fc3fcee94773ccc2bea74 100644 --- a/app/tables/recap.py +++ b/app/tables/recap.py @@ -86,22 +86,23 @@ class TableRecap(tb.Table): row.add_etud_cols() row.add_moyennes_cols(ues_sans_bonus) - self.add_partitions() - self.add_cursus() - self.add_admissions() - - # tri par rang croissant - if not res.formsemestre.block_moyenne_generale: - self.sort_rows(key=lambda row: row.rang_order) - else: - self.sort_rows(key=lambda row: row.nb_ues_validables, reverse=True) + if res.formsemestre.etuds_inscriptions: # table non vide + self.add_partitions() + self.add_cursus() + self.add_admissions() + + # tri par rang croissant + if not res.formsemestre.block_moyenne_generale: + self.sort_rows(key=lambda row: row.rang_order) + else: + self.sort_rows(key=lambda row: row.nb_ues_validables, reverse=True) - # Lignes footer (min, max, ects, apo, ...) - self.add_bottom_rows(ues_sans_bonus) + # Lignes footer (min, max, ects, apo, ...) + self.add_bottom_rows(ues_sans_bonus) - # Evaluations: - if include_evaluations: - self.add_evaluations() + # Evaluations: + if include_evaluations: + self.add_evaluations() if finalize: self.finalize() diff --git a/app/views/notes.py b/app/views/notes.py index 4ae34dd7688ac903041a4865857d5ef4aa45ab54..ce2f95b634d0161eb166e4b6906f6394745852e7 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -42,7 +42,7 @@ from flask_login import current_user from app import db from app import models from app.auth.models import User -from app.but import apc_edit_ue, jury_but_recap +from app.but import apc_edit_ue from app.but import jury_but, jury_but_validation_auto from app.but.forms import jury_but_forms from app.but import jury_but_pv @@ -60,6 +60,7 @@ from app.models.moduleimpls import ModuleImpl from app.models.modules import Module from app.models.ues import DispenseUE, UniteEns from app.scodoc.sco_exceptions import ScoFormationConflict +from app.tables import jury_recap from app.views import notes_bp as bp from app.decorators import ( @@ -2826,7 +2827,7 @@ def formsemestre_jury_but_recap(formsemestre_id: int, selected_etudid: int = Non raise ScoValueError( "formsemestre_jury_but_recap: réservé aux semestres pairs de BUT" ) - return jury_but_recap.formsemestre_saisie_jury_but( + return jury_recap.formsemestre_saisie_jury_but( formsemestre, read_only=read_only, selected_etudid=selected_etudid, mode="recap" )