# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2023 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 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, Module, ModuleImpl, NotesNotes from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ( ScoValueError, ScoInvalidDateError, ScoInvalidIdType, ) from app.scodoc import html_sco_header from app.scodoc import htmlutils from app.scodoc import sco_abs from app.scodoc import sco_archives from app.scodoc import sco_bulletins from app.scodoc import codes_cursus from app.scodoc import sco_compute_moy from app.scodoc import sco_edit_ue from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db 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_moduleimpl from app.scodoc import sco_permissions_check from app.scodoc import sco_preferences from app.scodoc import sco_users from app.scodoc.gen_tables import GenTable from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html import sco_version def _build_menu_stats(formsemestre_id): "Définition du menu 'Statistiques'" 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": True, }, { "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": True, # 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) -> str: """HTML to render menubar""" 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.ScoImplement) 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": 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": "Vérifier absences aux évaluations", "endpoint": "notes.formsemestre_check_absences_html", "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.ScoImplement), "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.ScoChangeFormation) and formsemestre.etat, "helpmsg": "", }, { "title": "Supprimer ce semestre", "endpoint": "notes.formsemestre_delete", "args": {"formsemestre_id": formsemestre_id}, "enabled": current_user.has_permission(Permission.ScoImplement), "helpmsg": "", }, ] # debug : if current_app.config["ENV"] == "development": 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.ScoEtudInscrit) 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.ScoEtudInscrit) 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.ScoEtudInscrit) 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.ScoEtudChangeAdr) and sco_preferences.get_preference("portal_url"), }, { "title": "Exporter table des étudiants", "endpoint": "scolar.groups_view", "args": { "format": "allxls", "group_ids": sco_groups.get_default_group( formsemestre_id, fix_if_missing=True ), }, }, { "title": "Vérifier inscriptions multiples", "endpoint": "notes.formsemestre_inscrits_ailleurs", "args": {"formsemestre_id": formsemestre_id}, }, ] menu_groupes = [ { "title": "Listes, photos, feuilles...", "endpoint": "scolar.groups_view", "args": {"formsemestre_id": formsemestre_id}, "enabled": True, "helpmsg": "Accès aux listes des groupes d'étudiants", }, { "title": "Modifier groupes et partitions", "endpoint": "scolar.partition_editor", "args": {"formsemestre_id": formsemestre_id}, "enabled": sco_groups.sco_permissions_check.can_change_groups( formsemestre_id ), "helpmsg": "Editeur de partitions", }, { "title": "Ancienne page édition partitions", "endpoint": "scolar.edit_partition_form", "args": {"formsemestre_id": formsemestre_id}, "enabled": sco_groups.sco_permissions_check.can_change_groups( formsemestre_id ), }, ] # 1 item / partition: partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False) submenu = [] enabled = ( sco_groups.sco_permissions_check.can_change_groups(formsemestre_id) 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}, }, ] 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.PVArchive.list_obj_archives(formsemestre_id), }, ] menu_stats = _build_menu_stats(formsemestre_id) H = [ '", ] return "\n".join(H) def retreive_formsemestre_from_request() -> int: """Cherche si on a de quoi déduire le semestre affiché à partir des arguments de la requête: formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id Returns None si pas défini. """ if request.method == "GET": args = request.args elif request.method == "POST": args = request.form else: return None formsemestre_id = None # Search formsemestre group_ids = args.get("group_ids", []) if "formsemestre_id" in args: formsemestre_id = args["formsemestre_id"] elif "moduleimpl_id" in args and args["moduleimpl_id"]: modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=args["moduleimpl_id"]) if not modimpl: return None # suppressed ? modimpl = modimpl[0] formsemestre_id = modimpl["formsemestre_id"] elif "evaluation_id" in args: E = sco_evaluation_db.do_evaluation_list( {"evaluation_id": args["evaluation_id"]} ) if not E: return None # evaluation suppressed ? E = E[0] modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] formsemestre_id = modimpl["formsemestre_id"] elif "group_id" in args: group = sco_groups.get_group(args["group_id"]) formsemestre_id = group["formsemestre_id"] elif group_ids: if group_ids: if isinstance(group_ids, str): group_id = group_ids else: # prend le semestre du 1er groupe de la liste: group_id = group_ids[0] group = sco_groups.get_group(group_id) formsemestre_id = group["formsemestre_id"] elif "partition_id" in args: partition = sco_groups.get_partition(args["partition_id"]) formsemestre_id = partition["formsemestre_id"] if not formsemestre_id: return None # no current formsemestre return int(formsemestre_id) # 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 += [ "jour", "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["jour"] = "É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: ue_info["Coef."] = ue.coefficient ue_info["Coef._class"] = "ue_coef" 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_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( moduleimpl_id=modimpl.id ) 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, "_Module_class": "scotext", "Inscrits": len(mod_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.id) 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["_jour_order"] = e["jour"].isoformat() e["jour"] = e["jour"].strftime("%d/%m/%Y") 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 = Evaluation.query.get(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( columns_ids=columns_ids, rows=rows, titles=titles, origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}", caption=title, html_caption=title, html_class="table_leftalign formsemestre_description", base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}", page_title=title, html_title=html_sco_header.html_sem_header( "Description du semestre", with_page_header=False ), pdf_title=title, preferences=sco_preferences.SemPreferences(formsemestre_id), ) def formsemestre_description( formsemestre_id, format="html", with_evals=False, with_parcours=False ): """Description du semestre sous forme de table exportable Liste des modules et de leurs coefficients """ with_evals = int(with_evals) tab = formsemestre_description_table( formsemestre_id, with_evals=with_evals, with_parcours=with_parcours ) tab.html_before_table = f"""
indiquer les évaluations indiquer les parcours BUT """ return tab.make_page(format=format) # genere liste html pour accès aux groupes de ce semestre def _make_listes_sem(formsemestre: FormSemestre, with_absences=True): # construit l'URL "destination" # (a laquelle on revient apres saisie absences) destination = url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ) # H = [] # pas de menu absences si pas autorise: if with_absences and not current_user.has_permission(Permission.ScoAbsChange): with_absences = False # H.append( f"""

Listes de {formsemestre.titre} ({formsemestre.mois_debut()} - {formsemestre.mois_fin()})

""" ) weekday = datetime.datetime.today().weekday() try: if with_absences: first_monday = sco_abs.ddmmyyyy( formsemestre.date_debut.strftime("%d/%m/%Y") ).prev_monday() form_abs_tmpl = f""" absences saisie par semaine
""" else: form_abs_tmpl = "" except ScoInvalidDateError: # dates incorrectes dans semestres ? form_abs_tmpl = "" # H.append('
') # Genere liste pour chaque partition (categorie de groupes) for partition in sco_groups.get_partitions_list(formsemestre.id): if not partition["partition_name"]: H.append("

Tous les étudiants

") else: H.append("

Groupes de %(partition_name)s

" % partition) partition_is_empty = True groups = sco_groups.get_partition_groups(partition) if groups: H.append("") for group in groups: n_members = len(sco_groups.get_group_members(group["group_id"])) if n_members == 0: continue # skip empty groups partition_is_empty = False group["url_etat"] = url_for( "absences.EtatAbsencesGr", group_ids=group["group_id"], debut=formsemestre.date_debut.strftime("%d/%m/%Y"), fin=formsemestre.date_fin.strftime("%d/%m/%Y"), scodoc_dept=g.scodoc_dept, ) if group["group_name"]: group["label"] = "groupe %(group_name)s" % group else: group["label"] = "liste" H.append( f""" """ ) if with_absences: H.append(form_abs_tmpl % group) H.append("") H.append("
") if partition_is_empty: H.append('

Aucun groupe peuplé dans cette partition') if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id): H.append( f""" (créer)""" ) H.append("

") if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id): H.append( f"""

Ajouter une partition

""" ) H.append("
") return "\n".join(H) def html_expr_diagnostic(diagnostics): """Affiche messages d'erreur des formules utilisateurs""" H = [] H.append('
Erreur dans des formules utilisateurs:
") return "".join(H) def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None): """En-tête HTML des pages "semestre" """ sem: FormSemestre = FormSemestre.query.get(formsemestre_id) if not sem: raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)") formation: Formation = sem.formation parcours = formation.get_cursus() page_title = page_title or "Modules de " H = [ html_sco_header.html_sem_header( page_title, with_page_header=False, with_h2=False ), f"""") if formation.is_apc(): # Affiche les parcours BUT cochés. Si aucun, tous ceux du référentiel. sem_parcours = sem.get_parcours_apc() H.append( f""" """ ) evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id) H.append( '") H.append("
Formation: {formation.titre} """, ] if sem.semestre_id >= 0: H.append(f", {parcours.SESSION_NAME} {sem.semestre_id}") if sem.modalite: H.append(f" en {sem.modalite}") if sem.etapes: H.append( f"""   (étape { sem.etapes_apo_str() or "-" })""" ) H.append("
Parcours: {', '.join(parcours.code for parcours in sem_parcours)}
Évaluations: %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides' % evals ) if evals["last_modif"]: H.append( " (dernière note saisie le %s)" % evals["last_modif"].strftime("%d/%m/%Y à %Hh%M") ) H.append("
") warnings = [] if evals["attente"]: warnings.append( """Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur indicative.""" ) if sem.bul_hide_xml: warnings.append("""Bulletins non publiés sur le portail. """) if sem.block_moyennes: warnings.append("Calcul des moyennes bloqué !") if sem.semestre_id >= 0 and not sem.est_sur_une_annee(): warnings.append("""Ce semestre couvre plusieurs années scolaires !""") if warnings: H += [ f"""
{warning}
""" for warning in warnings ] return "".join(H) def formsemestre_status(formsemestre_id=None, check_parcours=True): """Tableau de bord semestre HTML""" # porté du DTML if formsemestre_id is not None and not isinstance(formsemestre_id, int): raise ScoInvalidIdType( "formsemestre_bulletinetud: formsemestre_id must be an integer !" ) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) # S'assure que les groupes de parcours sont à jour: if int(check_parcours): formsemestre.setup_parcours_groups() modimpls = sco_moduleimpl.moduleimpl_withmodule_list( formsemestre_id=formsemestre_id ) nt = res_sem.load_formsemestre_results(formsemestre) # Construit la liste de tous les enseignants de ce semestre: mails_enseignants = set(u.email for u in formsemestre.responsables) for modimpl in modimpls: mails_enseignants.add(sco_users.user_info(modimpl["responsable_id"])["email"]) mails_enseignants |= set( [sco_users.user_info(m["ens_id"])["email"] for m in modimpl["ens"]] ) can_edit = formsemestre.can_be_edited_by(current_user) can_change_all_notes = current_user.has_permission(Permission.ScoEditAllNotes) or ( current_user.id in [resp.id for resp in formsemestre.responsables] ) use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) H = [ html_sco_header.sco_header( page_title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}" ), '
', formsemestre_status_head( formsemestre_id=formsemestre_id, page_title="Tableau de bord" ), formsemestre_warning_apc_setup(formsemestre, nt), formsemestre_warning_etuds_sans_note(formsemestre, nt) if can_change_all_notes else "", """

Tableau de bord : """, ] if formsemestre.est_courant(): H.append( """cliquez sur un module pour saisir des notes""" ) elif datetime.date.today() > formsemestre.date_fin: if formsemestre.etat: H.append( """semestre terminé mais non verrouillé""" ) else: H.append( """semestre pas encore commencé""" ) H.append("

") if sco_preferences.get_preference("bul_show_all_evals", formsemestre_id): H.append( """
Toutes évaluations (même incomplètes) visibles
""" ) if nt.expr_diagnostics: H.append(html_expr_diagnostic(nt.expr_diagnostics)) if nt.parcours.APC_SAE: # BUT: tableau ressources puis SAE ressources = [ m for m in modimpls if m["module"]["module_type"] == ModuleType.RESSOURCE ] saes = [m for m in modimpls if m["module"]["module_type"] == ModuleType.SAE] autres = [ m for m in modimpls if m["module"]["module_type"] not in (ModuleType.RESSOURCE, ModuleType.SAE) ] H += [ f"""
{_TABLEAU_MODULES_HEAD} Ressources {formsemestre_tableau_modules( ressources, nt, formsemestre, can_edit=can_edit, show_ues=False )} SAÉs """, formsemestre_tableau_modules( saes, nt, formsemestre, can_edit=can_edit, show_ues=False ), ] if autres: H += [ """ Autres modules """, formsemestre_tableau_modules( autres, nt, formsemestre, can_edit=can_edit, show_ues=False ), ] H += [_TABLEAU_MODULES_FOOT, "
"] else: # formations classiques: groupe par UE H += [ "

", _TABLEAU_MODULES_HEAD, formsemestre_tableau_modules( modimpls, nt, formsemestre, can_edit=can_edit, use_ue_coefs=use_ue_coefs, ), _TABLEAU_MODULES_FOOT, "

", ] if use_ue_coefs and not formsemestre.formation.is_apc(): H.append( """

utilise les coefficients d'UE pour calculer la moyenne générale.

""" ) # --- LISTE DES ETUDIANTS H += [ '
', _make_listes_sem(formsemestre), "
", ] # --- Lien mail enseignants: adrlist = list(mails_enseignants - {None, ""}) if adrlist: H.append( '

Courrier aux %d enseignants du semestre

' % (",".join(adrlist), len(adrlist)) ) return "".join(H) + html_sco_header.sco_footer() _TABLEAU_MODULES_HEAD = """ """ _TABLEAU_MODULES_FOOT = """
Code Module Inscrits Responsable Coefs. Évaluations
""" def formsemestre_tableau_modules( modimpls: list[dict], nt, formsemestre: FormSemestre, can_edit=True, show_ues=True, use_ue_coefs=False, ) -> str: "Lignes table HTML avec modules du semestre" H = [] prev_ue_id = None for modimpl in modimpls: mod: Module = Module.query.get(modimpl["module_id"]) moduleimpl_status_url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl["moduleimpl_id"], ) mod_descr = "Module " + (mod.titre or "") if mod.is_apc(): coef_descr = ", ".join( [f"{ue.acronyme}: {co}" for ue, co in mod.ue_coefs_list()] ) if coef_descr: mod_descr += " Coefs: " + coef_descr else: mod_descr += " (pas de coefficients) " else: mod_descr += ", coef. " + str(mod.coefficient) mod_ens = sco_users.user_info(modimpl["responsable_id"])["nomcomplet"] if modimpl["ens"]: mod_ens += " (resp.), " + ", ".join( [sco_users.user_info(e["ens_id"])["nomcomplet"] for e in modimpl["ens"]] ) mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( moduleimpl_id=modimpl["moduleimpl_id"] ) ue = modimpl["ue"] if show_ues and (prev_ue_id != ue["ue_id"]): prev_ue_id = ue["ue_id"] titre = ue["titre"] if use_ue_coefs: titre += f""" (coef. {ue["coefficient"] or 0.0})""" H.append( f""" {ue["acronyme"]} {titre} """ ) expr = sco_compute_moy.get_ue_expression( formsemestre.id, ue["ue_id"], html_quote=True ) if expr: H.append( f""" {expr} formule inutilisée en 9.2: supprimer""" ) H.append("") if modimpl["ue"]["type"] != codes_cursus.UE_STANDARD: fontorange = " fontorange" # style css additionnel else: fontorange = "" etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl["moduleimpl_id"]) # if nt.parcours.APC_SAE: # tbd style si module non conforme if ( etat["nb_evals_completes"] > 0 and etat["nb_evals_en_cours"] == 0 and etat["nb_evals_vides"] == 0 ): H.append(f'') else: H.append(f'') H.append( f""" {mod.code} {mod.abbrev or mod.titre or ""} {len(mod_inscrits)} { sco_users.user_info(modimpl["responsable_id"])["prenomnom"] } """ ) if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE): coefs = mod.ue_coefs_list(ues=formsemestre.get_ues()) H.append(f'') for coef in coefs: if coef[1] > 0: H.append( f"""""" ) else: H.append("""""") H.append("") H.append("") if mod.module_type in ( None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs ModuleType.STANDARD, ModuleType.RESSOURCE, ModuleType.SAE, ): H.append('') nb_evals = ( etat["nb_evals_completes"] + etat["nb_evals_en_cours"] + etat["nb_evals_vides"] ) if nb_evals != 0: H.append( f"""{nb_evals} prévues, {etat["nb_evals_completes"]} ok""" ) if etat["nb_evals_en_cours"] > 0: H.append( f""", { etat["nb_evals_en_cours"] } en cours""" ) if etat["attente"]: H.append( f""" [en attente]""" ) elif mod.module_type == ModuleType.MALUS: nb_malus_notes = sum( [ e["etat"]["nb_notes"] for e in nt.get_mod_evaluation_etat_list(modimpl["moduleimpl_id"]) ] ) H.append( f""" malus ({nb_malus_notes} notes) """ ) else: raise ValueError(f"Invalid module_type {mod.module_type}") # a bug H.append("") return "\n".join(H) # Expérimental def get_formsemestre_etudids_sans_notes( formsemestre: FormSemestre, res: ResultatsSemestre ) -> set[int]: """Les étudids d'étudiants de ce semestre n'ayant aucune note alors que d'autres en ont. """ # Il y a-t-il des notes déjà saisies ? nb_notes_sem = ( NotesNotes.query.join(Evaluation) .join(ModuleImpl) .filter_by(formsemestre_id=formsemestre.id) .count() ) if not nb_notes_sem: return set() etudids_sans_notes = set.intersection( *[ set.intersection(*m_res.evals_etudids_sans_note.values()) for m_res in res.modimpls_results.values() if m_res.evals_etudids_sans_note ] ) nb_sans_notes = len(etudids_sans_notes) if nb_sans_notes > 0 and nb_sans_notes < len( formsemestre.get_inscrits(include_demdef=False) ): return etudids_sans_notes return set() def formsemestre_warning_etuds_sans_note( formsemestre: FormSemestre, res: ResultatsSemestre ) -> str: """Vérifie si on est dans la situation où certains (mais pas tous) étudiants n'ont aucune note alors que d'autres en ont. Ce cas se produit typiquement quand on inscrit un étudiant en cours de semestre. Il est alors utile de proposer de mettre toutes ses notes à ABS, ATT ou EXC pour éviter de laisser toutes les évaluations "incomplètes". """ etudids_sans_notes = get_formsemestre_etudids_sans_notes(formsemestre, res) if not etudids_sans_notes: return "" nb_sans_notes = len(etudids_sans_notes) if nb_sans_notes < 5: # peu d'étudiants, affiche leurs noms etuds: list[Identite] = sorted( [Identite.get_etud(etudid) for etudid in etudids_sans_notes], key=lambda e: e.sort_key, ) noms = ", ".join( [ f"""{etud.nomprenom}""" for etud in etuds ] ) msg_etuds = ( f"""{noms} n'{"a" if nb_sans_notes == 1 else "ont"} aucune note :""" ) else: msg_etuds = f"""{nb_sans_notes} étudiants n'ont aucune note :""" return f"""
{msg_etuds} {"lui" if nb_sans_notes == 1 else "leur"} affecter des notes.
""" def formsemestre_note_etuds_sans_notes( formsemestre_id: int, code: str = None, etudid: int = None ): """Affichage et saisie des étudiants sans notes Si etudid est spécifié, traite un seul étudiant.""" formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() res: ResultatsSemestre = res_sem.load_formsemestre_results(formsemestre) etudids_sans_notes = get_formsemestre_etudids_sans_notes(formsemestre, res) if etudid: etudids_sans_notes = etudids_sans_notes.intersection({etudid}) etuds: list[Identite] = sorted( [Identite.get_etud(eid) for eid in etudids_sans_notes], key=lambda e: e.sort_key, ) if request.method == "POST": if not code in ("ATT", "EXC", "ABS"): raise ScoValueError("code invalide: doit être ATT, ABS ou EXC") for etud in etuds: formsemestre.etud_set_all_missing_notes(etud, code) flash(f"Notes de {len(etuds)} étudiants affectées à {code}") return redirect( url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ) ) if not etuds: if etudid is None: message = """

aucun étudiant sans notes

""" else: flash( f"""{Identite.get_etud(etudid).nomprenom} a déjà des notes""" ) return redirect( url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) ) else: noms = "
  • ".join( [ f"""{etud.nomprenom}""" for etud in etuds ] ) message = f"""

    Étudiants sans notes:

    """ return f""" {html_sco_header.sco_header( page_title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}" )}
    {formsemestre_status_head( formsemestre_id=formsemestre_id, page_title="Étudiants sans notes" )}
    {message}
    Mettre toutes les notes de {"ces étudiants" if len(etuds)> 1 else "cet étudiant"} à :
    {html_sco_header.sco_footer()} """