# -*- 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 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 codes_cursus from app.scodoc import sco_compute_moy 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_view", "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, 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": 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}, }, ] 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) H = [ '", ] return "\n".join(H) # 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=html_sco_header.html_sem_header( "Description du semestre", with_page_header=False ), origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}", page_title=title, pdf_title=title, preferences=sco_preferences.SemPreferences(formsemestre_id), rows=rows, table_id="formsemestre_description_table", titles=titles, ) def formsemestre_description( formsemestre_id, fmt="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(fmt=fmt) # genere liste html pour accès aux groupes de ce semestre def _make_listes_sem(formsemestre: FormSemestre) -> str: """La section avec les groupes et l'assiduité""" H = [] # pas de menu absences si pas autorise: can_edit_abs = current_user.has_permission(Permission.AbsChange) # H.append( f"""

Groupes et absences de {formsemestre.titre} ({ formsemestre.mois_debut()} - {formsemestre.mois_fin() })

""" ) # H.append('
') disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre) show_abs: str = "hidden" if disable_abs else "" # Genere liste pour chaque partition (categorie de groupes) for partition in formsemestre.get_partitions_list(): groups = partition.groups.all() effectifs = {g.id: g.get_nb_inscrits() for g in groups} partition_is_empty = sum(effectifs.values()) == 0 H.append( f"""
{ 'Groupes de ' + partition.partition_name if partition.partition_name else ('aucun étudiant inscrit' if partition_is_empty else 'Tous les étudiants')}
{ "Assiduité" if not partition_is_empty and not show_abs else "" }
""" ) if groups: for group in groups: n_members = effectifs[group.id] if n_members == 0: continue # skip empty groups partition_is_empty = False group_label = f"{group.group_name}" if group.group_name else "liste" H.append( f"""
""" ) if can_edit_abs: H.append( f"""
Saisir l'assiduité
""" ) # YYYY-Www (ISO 8601) : current_week: str = datetime.datetime.now().strftime("%G-W%V") H.append( f"""
Saisie hebdomadaire
""" ) if can_edit_abs: H.append( f"""
Justificatifs en attente
""" ) H.append( f"""
Bilan
""" ) H.append("
") # /sem-groups-assi if partition_is_empty and not partition.is_default(): H.append( '
Aucun groupe peuplé dans cette partition' ) if formsemestre.can_change_groups(): H.append( f""" (créer)""" ) H.append("
") H.append("
") # /sem-groups-partition if formsemestre.can_change_groups(): H.append( f"""

Ajouter une partition

""" ) # --- Formulaire importation Assiduité excel (si autorisé) if current_user.has_permission(Permission.AbsChange) and not disable_abs: H.append( f"""

Importation de l'assiduité depuis un fichier excel

""" ) # --- Lien Traitement Justificatifs: if ( current_user.has_permission(Permission.AbsJustifView) and current_user.has_permission(Permission.JustifValidate) and not disable_abs ): H.append( f"""

Traitement des justificatifs d'absence

""" ) H.append("
") if disable_abs: H.append( f"""
La gestion des absences est désactivée dans ScoDoc pour ce semestre: {disable_abs}
""" ) return "\n".join(H) def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None): """En-tête HTML des pages "semestre" """ formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) if not formsemestre: raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)") formation: Formation = formsemestre.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 = formsemestre.get_parcours_apc() H.append( f""" """ ) evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre) H.append( '") H.append("
Formation: {formation.titre} """, ] if formsemestre.semestre_id >= 0: H.append(f", {parcours.SESSION_NAME} {formsemestre.semestre_id}") if formsemestre.modalite: H.append(f" en {formsemestre.modalite}") if formsemestre.etapes: H.append( f"""   (étape { formsemestre.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 formsemestre.bul_hide_xml: warnings.append("""Bulletins non publiés sur la passerelle.""") if formsemestre.block_moyennes: warnings.append("Calcul des moyennes bloqué !") if formsemestre.semestre_id >= 0 and not formsemestre.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 = formsemestre.modimpls_sorted 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 |= {u.email for u in modimpl.enseignants if u.email} can_edit = formsemestre.can_be_edited_by(current_user) can_change_all_notes = current_user.has_permission(Permission.EditAllNotes) 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.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 # élimine les modules BUT qui aurait pu se glisser là suite à un # changement de type de formation par exemple modimpls_classic = [ m for m in modimpls if m.module.module_type not in (ModuleType.RESSOURCE, ModuleType.SAE) ] H += [ "

", _TABLEAU_MODULES_HEAD, formsemestre_tableau_modules( modimpls_classic, 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( f"""

Courrier aux { len(adrlist)} enseignants du semestre

""" ) 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[ModuleImpl], 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 = modimpl.module moduleimpl_status_url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, ) mod_descr = "Module " + (mod.titre or "") is_apc = mod.is_apc() # SAE ou ressource if is_apc: mod_descr += " " + mod.get_ue_coefs_descr() else: mod_descr += ", coef. " + str(mod.coefficient) mod_ens = sco_users.user_info(modimpl.responsable_id)["nomcomplet"] if modimpl.enseignants.count(): mod_ens += " (resp.), " + ", ".join( [u.get_nomcomplet() for u in modimpl.enseignants] ) mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module mod_is_conforme = modimpl.check_apc_conformity(nt) ue = modimpl.module.ue if show_ues and (prev_ue_id != ue.id): prev_ue_id = ue.id titre = ue.titre or "" 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.id, html_quote=True ) if expr: H.append( f""" {expr} formule inutilisée en ScoDoc 9: supprimer""" ) H.append("") if ue.type != codes_cursus.UE_STANDARD: fontorange = " fontorange" # style css additionnel else: fontorange = "" etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl) if ( etat["nb_evals_completes"] > 0 and etat["nb_evals_en_cours"] == 0 and etat["nb_evals_vides"] == 0 and not etat["attente"] and not etat["nb_evals_blocked"] > 0 ): tr_classes = f"formsemestre_status_green{fontorange}" else: tr_classes = f"formsemestre_status{fontorange}" if etat["attente"]: tr_classes += " modimpl_attente" if not mod_is_conforme: tr_classes += " modimpl_non_conforme" if etat["nb_evals_blocked"] > 0: tr_classes += " modimpl_has_blocked" H.append( f""" {mod.code} {mod.abbrev or mod.titre or ""} {mod_nb_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"] if nb_evals != 0: if etat["nb_evals_blocked"] > 0: blocked_txt = f"""{ etat["nb_evals_blocked"]} bloquée{'s' if etat["nb_evals_blocked"] > 1 else ''}""" else: blocked_txt = "" H.append( f"""{nb_evals} prévues, {etat["nb_evals_completes"]} ok {blocked_txt} """ ) if etat["nb_evals_en_cours"] > 0: H.append( f""", { etat["nb_evals_en_cours"] } en cours""" ) if etat["attente"]: H.append( f""" { etat["nb_evals_attente"] } en attente""" ) if not mod_is_conforme: H.append( f""" [non conforme]""" ) elif mod.module_type == ModuleType.MALUS: nb_malus_notes = sum( e["etat"]["nb_notes"] for e in nt.get_mod_evaluation_etat_list(modimpl) ) 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() notes_modimpls = [ set.intersection(*m_res.evals_etudids_sans_note.values()) for m_res in res.modimpls_results.values() if m_res.evals_etudids_sans_note ] if not notes_modimpls: return set() etudids_sans_notes = set.intersection(*notes_modimpls) 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""" """ 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.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid) ) else: noms = "
  • ".join( [ f"""{etud.nomprenom}""" for etud in etuds ] ) message = f"""

    Étudiants sans notes:

    • {noms}
    """ 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()} """