# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # 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 # ############################################################################## """Tableau de bord semestre""" import datetime from flask import current_app from flask import g from flask import request from flask import flash, redirect, render_template, url_for from flask_login import current_user from app import db, log from app.but.cursus_but import formsemestre_warning_apc_setup from app.comp import res_sem from app.comp.res_common import ResultatsSemestre from app.comp.res_compat import NotesTableCompat from app.models import ( Evaluation, Formation, FormSemestre, Identite, Module, ModuleImpl, NotesNotes, ) from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.sco_exceptions import ( ScoValueError, ScoInvalidIdType, ) from app.scodoc.sco_permissions import Permission import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType from app.scodoc import codes_cursus from app.scodoc import html_sco_header from app.scodoc import htmlutils from app.scodoc import sco_archives_formsemestre from app.scodoc import sco_assiduites as scass from app.scodoc import sco_bulletins from app.scodoc import sco_cache from app.scodoc import sco_evaluations from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_preferences from app.scodoc import sco_users from app.scodoc.gen_tables import GenTable from app.scodoc.html_sidebar import retreive_formsemestre_from_request from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html import sco_version def _build_menu_stats(formsemestre: FormSemestre): "Définition du menu 'Statistiques'" formsemestre_id = formsemestre.id return [ { "title": "Statistiques...", "endpoint": "notes.formsemestre_report_counts", "args": {"formsemestre_id": formsemestre_id}, }, { "title": "Suivi de cohortes", "endpoint": "notes.formsemestre_suivi_cohorte", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, }, { "title": "Graphe des cursus", "endpoint": "notes.formsemestre_graph_cursus", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, }, { "title": "Codes des cursus", "endpoint": "notes.formsemestre_suivi_cursus", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, }, { "title": "Lycées d'origine", "endpoint": "notes.formsemestre_etuds_lycees", "args": {"formsemestre_id": formsemestre_id}, "enabled": current_user.has_permission(Permission.ViewEtudData), }, { "title": 'Table "poursuite"', "endpoint": "notes.formsemestre_poursuite_report", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, }, { "title": "Documents Avis Poursuite Etudes (xp)", "endpoint": "notes.pe_view_sem_recap", "args": {"formsemestre_id": formsemestre_id}, "enabled": formsemestre.formation.is_apc(), # current_app.config["TESTING"] or current_app.config["DEBUG"], }, { "title": 'Table "débouchés"', "endpoint": "notes.report_debouche_date", "enabled": True, }, { "title": "Estimation du coût de la formation", "endpoint": "notes.formsemestre_estim_cost", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, }, { "title": "Indicateurs de suivi annuel BUT", "endpoint": "notes.formsemestre_but_indicateurs", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, }, ] def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str: """HTML to render menubar""" if formsemestre is None: return "" formsemestre_id = formsemestre.id if formsemestre.etat: change_lock_msg = "Verrouiller" else: change_lock_msg = "Déverrouiller" formation = formsemestre.formation # L'utilisateur est-il resp. du semestre ? is_responsable = current_user.id in (u.id for u in formsemestre.responsables) # A le droit de changer le semestre (déverrouiller, préférences bul., ...): has_perm_change_sem = current_user.has_permission(Permission.EditFormSemestre) or ( formsemestre.resp_can_edit and is_responsable ) # Peut modifier le semestre (si n'est pas verrouillé): can_modify_sem = has_perm_change_sem and formsemestre.etat menu_semestre = [ { "title": "Tableau de bord", "endpoint": "notes.formsemestre_status", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, "helpmsg": "Tableau de bord du semestre", }, # { # "title": "Assiduité du semestre", # "endpoint": "assiduites.liste_assiduites_formsemestre", # "args": {"formsemestre_id": formsemestre_id}, # "enabled": True, # "helpmsg": "Tableau de l'assiduité et des justificatifs du semestre", # }, { "title": f"Voir la formation {formation.acronyme} (v{formation.version})", "endpoint": "notes.ue_table", "args": { "formation_id": formation.id, "semestre_idx": formsemestre.semestre_id, }, "enabled": True, "helpmsg": "Tableau de bord du semestre", }, { "title": "Modifier le semestre", "endpoint": "notes.formsemestre_editwithmodules", "args": { "formsemestre_id": formsemestre_id, }, "enabled": can_modify_sem, "helpmsg": "Modifie le contenu du semestre (modules)", }, { "title": "Préférences du semestre", "endpoint": "scolar.formsemestre_edit_preferences", "args": {"formsemestre_id": formsemestre_id}, "enabled": can_modify_sem, "helpmsg": "Préférences du semestre", }, { "title": "Réglages bulletins", "endpoint": "notes.formsemestre_edit_options", "args": {"formsemestre_id": formsemestre_id}, "enabled": has_perm_change_sem, "helpmsg": "Change les options", }, { "title": change_lock_msg, "endpoint": "notes.formsemestre_flip_lock", "args": {"formsemestre_id": formsemestre_id}, "enabled": has_perm_change_sem, "helpmsg": "", }, { "title": "Description du semestre", "endpoint": "notes.formsemestre_description", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, "helpmsg": "", }, { "title": "Lister tous les enseignants", "endpoint": "notes.formsemestre_enseignants_list", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, "helpmsg": "", }, { "title": "Cloner ce semestre", "endpoint": "notes.formsemestre_clone", "args": {"formsemestre_id": formsemestre_id}, "enabled": current_user.has_permission(Permission.EditFormSemestre), "helpmsg": "", }, { "title": "Associer à une nouvelle version du programme", "endpoint": "notes.formsemestre_associate_new_version", "args": { "formsemestre_id": formsemestre_id, "formation_id": formsemestre.formation_id, }, "enabled": current_user.has_permission(Permission.EditFormation) and formsemestre.etat, "helpmsg": "", }, { "title": "Supprimer ce semestre", "endpoint": "notes.formsemestre_delete", "args": {"formsemestre_id": formsemestre_id}, "enabled": current_user.has_permission(Permission.EditFormSemestre), "helpmsg": "", }, { "title": "Expérimental: emploi du temps", "endpoint": "notes.formsemestre_edt", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, "helpmsg": "", }, ] # debug : if current_app.config["DEBUG"]: menu_semestre.append( { "title": "Vérifier l'intégrité", "endpoint": "notes.check_sem_integrity", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, } ) menu_inscriptions = [ { "title": ( "Gérer les inscriptions aux UE et modules" if formsemestre.formation.is_apc() else "Gérer les inscriptions aux modules" ), "endpoint": "notes.moduleimpl_inscriptions_stats", "args": {"formsemestre_id": formsemestre_id}, } ] menu_inscriptions += [ { "title": "Passage des étudiants depuis d'autres semestres", "endpoint": "notes.formsemestre_inscr_passage", "args": {"formsemestre_id": formsemestre_id}, "enabled": current_user.has_permission(Permission.EtudInscrit) and formsemestre.etat, }, { "title": "Synchroniser avec étape Apogée", "endpoint": "notes.formsemestre_synchro_etuds", "args": {"formsemestre_id": formsemestre_id}, "enabled": current_user.has_permission(Permission.ScoView) and sco_preferences.get_preference("portal_url") and formsemestre.etat, }, { "title": "Inscrire un étudiant", "endpoint": "notes.formsemestre_inscription_with_modules_etud", "args": {"formsemestre_id": formsemestre_id}, "enabled": current_user.has_permission(Permission.EtudInscrit) and formsemestre.etat, }, { "title": "Importer des étudiants dans ce semestre (table Excel)", "endpoint": "scolar.form_students_import_excel", "args": {"formsemestre_id": formsemestre_id}, "enabled": current_user.has_permission(Permission.EtudInscrit) and formsemestre.etat, }, { "title": "Import/export des données admission", "endpoint": "scolar.form_students_import_infos_admissions", "args": {"formsemestre_id": formsemestre_id}, "enabled": current_user.has_permission(Permission.ScoView), }, { "title": "Resynchroniser données identité", "endpoint": "scolar.formsemestre_import_etud_admission", "args": {"formsemestre_id": formsemestre_id}, "enabled": current_user.has_permission(Permission.EtudChangeAdr) and sco_preferences.get_preference("portal_url"), }, { "title": "Exporter table des étudiants", "endpoint": "scolar.groups_lists", "args": { "fmt": "allxls", "group_ids": sco_groups.get_default_group( formsemestre_id, fix_if_missing=True ), }, "enabled": current_user.has_permission(Permission.ViewEtudData), }, { "title": "Vérifier inscriptions multiples", "endpoint": "notes.formsemestre_inscrits_ailleurs", "args": {"formsemestre_id": formsemestre_id}, }, ] can_change_groups = formsemestre.can_change_groups() menu_groupes = [ { "title": "Listes des groupes", "endpoint": "scolar.groups_lists", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, "helpmsg": "Accès aux listes des groupes d'étudiants", }, { "title": "Trombinoscopes", "endpoint": "scolar.groups_photos", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, "helpmsg": "Accès aux photos des groupes d'étudiants", }, { "title": "Assiduité, feuilles d'appel, ...", "endpoint": "scolar.groups_feuilles", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, "helpmsg": "Accès aux feuilles d'appel des groupes d'étudiants", }, { "title": "Modifier groupes et partitions", "endpoint": "scolar.partition_editor", "args": {"formsemestre_id": formsemestre_id}, "enabled": can_change_groups, "helpmsg": "Editeur de partitions", }, { "title": "Ancienne page édition partitions", "endpoint": "scolar.edit_partition_form", "args": {"formsemestre_id": formsemestre_id}, "enabled": can_change_groups, }, ] # 1 item / partition: partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False) submenu = [] enabled = can_change_groups and partitions for partition in partitions: submenu.append( { "title": str(partition["partition_name"]), "endpoint": "scolar.affect_groups", "args": {"partition_id": partition["partition_id"]}, "enabled": enabled, } ) menu_groupes.append( { "title": "Ancienne page édition groupes", "submenu": submenu, "enabled": enabled, } ) menu_notes = [ { "title": "Tableau des moyennes (et liens bulletins)", "endpoint": "notes.formsemestre_recapcomplet", "args": {"formsemestre_id": formsemestre_id}, }, { "title": "État des évaluations", "endpoint": "notes.evaluations_recap", "args": {"formsemestre_id": formsemestre_id}, }, { "title": "Classeur PDF des bulletins", "endpoint": "notes.formsemestre_bulletins_pdf_choice", "args": {"formsemestre_id": formsemestre_id}, "helpmsg": "PDF regroupant tous les bulletins", }, { "title": "Envoyer à chaque étudiant son bulletin par e-mail", "endpoint": "notes.formsemestre_bulletins_mailetuds_choice", "args": {"formsemestre_id": formsemestre_id}, "enabled": sco_bulletins.can_send_bulletin_by_mail(formsemestre_id), }, { "title": "Calendrier des évaluations", "endpoint": "notes.formsemestre_evaluations_cal", "args": {"formsemestre_id": formsemestre_id}, }, { "title": "Lister toutes les saisies de notes", "endpoint": "notes.formsemestre_list_saisies_notes", "args": {"formsemestre_id": formsemestre_id}, }, { "title": "Importer les notes", "endpoint": "notes.formsemestre_import_notes", "args": {"formsemestre_id": formsemestre_id}, "enabled": formsemestre.est_chef_or_diretud(), }, ] menu_jury = [ { "title": "Voir les décisions du jury", "endpoint": "notes.formsemestre_pvjury", "args": {"formsemestre_id": formsemestre_id}, }, { "title": "Saisie des décisions du jury", "endpoint": "notes.formsemestre_recapcomplet", "args": { "formsemestre_id": formsemestre_id, "mode_jury": 1, }, "enabled": formsemestre.can_edit_jury(), }, { "title": "Générer feuille préparation Jury (non BUT)", "endpoint": "notes.feuille_preparation_jury", "args": {"formsemestre_id": formsemestre_id}, "enabled": not formsemestre.formation.is_apc(), }, { "title": "Éditer les PV et archiver les résultats", "endpoint": "notes.formsemestre_archive", "args": {"formsemestre_id": formsemestre_id}, "enabled": formsemestre.can_edit_pv(), }, { "title": "Documents archivés", "endpoint": "notes.formsemestre_list_archives", "args": {"formsemestre_id": formsemestre_id}, "enabled": sco_archives_formsemestre.PV_ARCHIVER.list_obj_archives( formsemestre_id ), }, ] menu_stats = _build_menu_stats(formsemestre) menus = { "Semestre": menu_semestre, "Inscriptions": menu_inscriptions, "Groupes": menu_groupes, "Notes": menu_notes, "Jury": menu_jury, "Statistiques": menu_stats, "Liens": formsemestre_custommenu_html(formsemestre_id), } return render_template( "formsemestre/menu.j2", menu=menus, formsemestre=formsemestre ) # Element HTML decrivant un semestre (barre de menu et infos) def formsemestre_page_title(formsemestre_id=None): """Element HTML decrivant un semestre (barre de menu et infos) Cherche dans la requete si un semestre est défini via (formsemestre_id ou moduleimpl ou evaluation ou group) """ formsemestre_id = ( formsemestre_id if formsemestre_id is not None else retreive_formsemestre_from_request() ) # if not formsemestre_id: return "" try: formsemestre_id = int(formsemestre_id) except ValueError: log(f"formsemestre_id: invalid type {formsemestre_id:r}") return "" formsemestre = FormSemestre.get_formsemestre(formsemestre_id) return render_template( "formsemestre_page_title.j2", formsemestre=formsemestre, scu=scu, sem_menu_bar=formsemestre_status_menubar(formsemestre), ) # --------- # ancienne fonction ScoDoc7 à supprimer lorsqu'on utilisera les modèles # utilisé seulement par export Apogée def fill_formsemestre(sem: dict): # XXX OBSOLETE """Add some fields in formsemestres dicts""" formsemestre_id = sem["formsemestre_id"] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] sem["formation"] = F parcours = codes_cursus.get_cursus_from_code(F["type_parcours"]) if sem["semestre_id"] != -1: sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}""" else: sem["num_sem"] = "" # formation sans semestres if sem["modalite"]: sem["modalitestr"] = f""" en {sem["modalite"]}""" else: sem["modalitestr"] = "" sem["etape_apo_str"] = "Code étape Apogée: " + ( sco_formsemestre.formsemestre_etape_apo_str(sem) or "Pas de code étape" ) inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( args={"formsemestre_id": formsemestre_id} ) sem["nbinscrits"] = len(inscrits) uresps = [ sco_users.user_info(responsable_id) for responsable_id in sem["responsables"] ] sem["resp"] = ", ".join([u["prenomnom"] for u in uresps]) sem["nomcomplet"] = ", ".join([u["nomcomplet"] for u in uresps]) # Description du semestre sous forme de table exportable def formsemestre_description_table( formsemestre_id: int, with_evals=False, with_parcours=False ): """Description du semestre sous forme de table exportable Liste des modules et de leurs coefficients """ formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() is_apc = formsemestre.formation.is_apc() nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours) # --- Colonnes à proposer: columns_ids = ["UE", "Code", "Module"] if with_parcours: columns_ids += ["parcours"] if not formsemestre.formation.is_apc(): columns_ids += ["Coef."] ues = [] # liste des UE, seulement en APC pour les coefs else: ues = formsemestre.get_ues() columns_ids += [f"ue_{ue.id}" for ue in ues] if sco_preferences.get_preference("bul_show_ects", formsemestre_id) and not is_apc: columns_ids += ["ects"] columns_ids += ["Inscrits", "Responsable", "Enseignants"] if with_evals: columns_ids += [ "date_evaluation", "description", "coefficient", "evalcomplete_str", "publish_incomplete_str", ] titles = {title: title for title in columns_ids} titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues}) titles["ects"] = "ECTS" titles["date_evaluation"] = "Évaluation" titles["description"] = "" titles["coefficient"] = "Coef. éval." titles["evalcomplete_str"] = "Complète" titles["parcours"] = "Parcours" titles["publish_incomplete_str"] = "Toujours utilisée" title = f"{parcours.SESSION_NAME.capitalize()} {formsemestre.titre_mois()}" rows = [] sum_coef = 0 sum_ects = 0 last_ue_id = None formsemestre_parcours_ids = {p.id for p in formsemestre.parcours} for modimpl in formsemestre.modimpls_sorted: # Ligne UE avec ECTS: ue = modimpl.module.ue if ue.id != last_ue_id: last_ue_id = ue.id if ue.ects is None: ects_str = "-" else: sum_ects += ue.ects ects_str = ue.ects ue_info = { "UE": ue.acronyme, "Code": "", "ects": ects_str, "Module": ue.titre, "_css_row_class": "table_row_ue", } if use_ue_coefs and ue.type != UE_SPORT: ue_info["Coef."] = ue.coefficient or "0." ue_info["_Coef._class"] = "ue_coef" if not ue.coefficient: ue_info["_Coef._class"] += " ue_coef_nul" if ue.color: for k in list(ue_info.keys()): if not k.startswith("_"): ue_info[f"_{k}_td_attrs"] = ( f'style="background-color: {ue.color} !important;"' ) if not is_apc: # n'affiche la ligne UE qu'en formation classique # car l'UE de rattachement n'a pas d'intérêt en BUT rows.append(ue_info) mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module enseignants = ", ".join(ens.get_prenomnom() for ens in modimpl.enseignants) row = { "UE": modimpl.module.ue.acronyme, "_UE_td_attrs": ue_info.get("_UE_td_attrs", ""), "Code": modimpl.module.code or "", "Module": modimpl.module.abbrev or modimpl.module.titre or "", "_Module_class": "scotext", "Inscrits": mod_nb_inscrits, "Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"], "_Responsable_class": "scotext", "Enseignants": enseignants, "_Enseignants_class": "scotext", "Coef.": modimpl.module.coefficient, # 'ECTS' : M['module']['ects'], # Lien sur titre -> module "_Module_target": url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, ), "_Code_target": url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, ), } if modimpl.module.coefficient is not None: sum_coef += modimpl.module.coefficient coef_dict = modimpl.module.get_ue_coef_dict() for ue in ues: row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or "" if with_parcours: # Intersection des parcours du module avec ceux du formsemestre row["parcours"] = ", ".join( [ pa.code for pa in ( modimpl.module.parcours if modimpl.module.parcours else modimpl.formsemestre.parcours ) if pa.id in formsemestre_parcours_ids ] ) rows.append(row) if with_evals: # Ajoute lignes pour evaluations evals = nt.get_mod_evaluation_etat_list(modimpl) evals.reverse() # ordre chronologique # Ajoute etat: eval_rows = [] for eval_dict in evals: e = eval_dict.copy() e["_description_target"] = url_for( "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, evaluation_id=e["evaluation_id"], ) e["_date_evaluation_order"] = e["jour"].isoformat() e["date_evaluation"] = ( e["jour"].strftime(scu.DATE_FMT) if e["jour"] else "" ) e["UE"] = row["UE"] e["_UE_td_attrs"] = row["_UE_td_attrs"] e["Code"] = row["Code"] e["_css_row_class"] = "evaluation" e["Module"] = "éval." # Cosmetic: conversions pour affichage if e["etat"]["evalcomplete"]: e["evalcomplete_str"] = "Oui" e["_evalcomplete_str_td_attrs"] = 'style="color: green;"' else: e["evalcomplete_str"] = "Non" e["_evalcomplete_str_td_attrs"] = 'style="color: red;"' if e["publish_incomplete"]: e["publish_incomplete_str"] = "Oui" e["_publish_incomplete_str_td_attrs"] = 'style="color: green;"' else: e["publish_incomplete_str"] = "Non" e["_publish_incomplete_str_td_attrs"] = 'style="color: red;"' # Poids vers UEs (en APC) evaluation: Evaluation = db.session.get(Evaluation, e["evaluation_id"]) for ue_id, poids in evaluation.get_ue_poids_dict().items(): e[f"ue_{ue_id}"] = poids or "" e[f"_ue_{ue_id}_class"] = "poids" e[f"_ue_{ue_id}_help"] = "poids vers l'UE" eval_rows.append(e) rows += eval_rows sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef} rows.append(sums) return GenTable( base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}", caption=title, columns_ids=columns_ids, html_caption=title, html_class="table_leftalign formsemestre_description", html_title="