# -*- 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 module""" import math import datetime from flask import g, render_template, url_for from flask_login import current_user from app import db, log from app.auth.models import User 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, FormSemestre, Module, ModuleImpl, UniteEns import app.scodoc.sco_utils as scu from app.scodoc import sco_assiduites as scass from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.sco_cursus_dut import formsemestre_has_decisions from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc import htmlutils from app.scodoc import sco_evaluations from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.tables import list_etuds # menu evaluation dans moduleimpl def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str: "Menu avec actions sur une evaluation" modimpl: ModuleImpl = evaluation.moduleimpl group_id = sco_groups.get_default_group(modimpl.formsemestre_id) evaluation_id = evaluation.id can_edit_notes = modimpl.can_edit_notes(current_user, allow_ens=False) can_edit_notes_ens = modimpl.can_edit_notes(current_user) if can_edit_notes and nbnotes != 0: sup_label = "Suppression évaluation impossible (il y a des notes)" else: sup_label = "Supprimer évaluation" formsemestre: FormSemestre = FormSemestre.get_formsemestre(modimpl.formsemestre_id) disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre) menu_eval = [ { "title": "Saisir les notes", "endpoint": "notes.form_saisie_notes", "args": { "evaluation_id": evaluation_id, }, "enabled": can_edit_notes_ens, }, { "title": "Saisir par fichier tableur", "id": "menu_saisie_tableur", "endpoint": "notes.saisie_notes_tableur", "args": { "evaluation_id": evaluation.id, }, }, { "title": "Modifier évaluation", "endpoint": "notes.evaluation_edit", "args": { "evaluation_id": evaluation_id, }, "enabled": can_edit_notes, }, { "title": sup_label, "endpoint": "notes.evaluation_delete", "args": { "evaluation_id": evaluation_id, }, "enabled": nbnotes == 0 and can_edit_notes, }, { "title": "Supprimer toutes les notes", "endpoint": "notes.evaluation_suppress_alln", "args": { "evaluation_id": evaluation_id, }, "enabled": can_edit_notes, }, { "title": "Afficher les notes", "endpoint": "notes.evaluation_listenotes", "args": { "evaluation_id": evaluation_id, }, "enabled": nbnotes > 0, }, { "title": "Placement étudiants", "endpoint": "notes.placement_eval_selectetuds", "args": { "evaluation_id": evaluation_id, }, "enabled": can_edit_notes_ens, }, { "title": "Absences ce jour", "endpoint": "assiduites.etat_abs_date", "args": { "group_ids": group_id, "evaluation_id": evaluation.id, "date_debut": ( evaluation.date_debut.isoformat() if evaluation.date_debut else "" ), "date_fin": ( evaluation.date_fin.isoformat() if evaluation.date_fin else "" ), }, "enabled": evaluation.date_debut is not None and evaluation.date_fin is not None and not disable_abs, }, { "title": "Vérifier notes vs absents", "endpoint": "notes.evaluation_check_absences_html", "args": { "evaluation_id": evaluation_id, }, "enabled": nbnotes > 0 and evaluation.date_debut is not None and not disable_abs, }, ] return htmlutils.make_menu("actions", menu_eval, alone=True) def _ue_coefs_html(modimpl: ModuleImpl) -> str: """ """ coefs_lst = modimpl.module.ue_coefs_list() max_coef = max(x[1] for x in coefs_lst) if coefs_lst else 1.0 H = f"""
Coefficients vers les UEs détail
""" if coefs_lst: H += _html_hinton_map( colors=(uc[0].color for uc in coefs_lst), max_val=max_coef, size=36, title=modimpl.module.get_ue_coefs_descr(), values=(uc[1] for uc in coefs_lst), ) # ( # f""" #
# """ # + "\n".join( # [ # f"""
{coef}
{ue.acronyme}
""" # for ue, coef in coefs_lst # if coef > 0 # ] # ) # + "
" # ) else: H += """
non définis
""" H += "
" return H def moduleimpl_status(moduleimpl_id=None, partition_id=None): """Tableau de bord module (liste des evaluations etc)""" modimpl: ModuleImpl = ModuleImpl.get_modimpl(moduleimpl_id) g.current_moduleimpl_id = modimpl.id module: Module = modimpl.module formsemestre_id = modimpl.formsemestre_id formsemestre: FormSemestre = modimpl.formsemestre mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( moduleimpl_id=moduleimpl_id ) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) # Evaluations, par numéros ou la plus RECENTE en tête evaluations = modimpl.evaluations.order_by( Evaluation.numero.desc(), Evaluation.date_debut.desc(), ).all() nb_evaluations = len(evaluations) # Le poids max pour chaque catégorie d'évaluation max_poids_by_type: dict[int, float] = {} for eval_type in ( Evaluation.EVALUATION_NORMALE, Evaluation.EVALUATION_RATTRAPAGE, Evaluation.EVALUATION_SESSION2, Evaluation.EVALUATION_BONUS, ): max_poids_by_type[eval_type] = max( [ max([p.poids for p in e.ue_poids] or [0]) * (e.coefficient or 0.0) for e in evaluations if e.evaluation_type == eval_type ] or [0.0] ) # sem_locked = not formsemestre.etat can_edit_evals = ( modimpl.can_edit_notes(current_user, allow_ens=formsemestre.ens_can_edit_eval) and not sem_locked ) can_edit_notes = modimpl.can_edit_notes(current_user) and not sem_locked arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags() # module_resp = db.session.get(User, modimpl.responsable_id) if module_resp is None: # l'utilisateur a été supprimé par erreur # ré-affecte le module au 1er resp. du semestre if modimpl.formsemestre.responsables: log( f"""Modimpl resp. {modimpl} suppressed: affecting to { modimpl.formsemestre.responsables[0]}""" ) modimpl.responsable_id = modimpl.formsemestre.responsables[0].id db.session.add(modimpl) db.session.commit() else: raise ScoValueError( "Module et semestre sans responsables: contacter l'assistance" ) mod_type_name = scu.MODULE_TYPE_NAMES[module.module_type] H = [ f"""

{mod_type_name} {module.code} {module.titre} {"dans l'UE " + modimpl.module.ue.acronyme if modimpl.module.module_type == scu.ModuleType.MALUS else "" }

""") # 2ieme ligne: Semestre, Coef H.append("""""") # 3ieme ligne: Formation H.append( f""" """ ) # Ligne: Inscrits H.append( f""" """ ) disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre) if not disable_abs: H.append( f"""
Responsable: {module_resp.get_nomcomplet()} ({module_resp.user_name}) """, ] if modimpl.can_change_responsable(current_user): H.append( f"""modifier""" ) H.append("""""") H.append(", ".join([u.get_nomprenom() for u in modimpl.enseignants])) H.append("""""") if modimpl.can_change_ens(raise_exc=False): H.append( f"""modifier les enseignants""" ) H.append("""
""") if formsemestre.semestre_id >= 0: H.append(f"""Semestre: {formsemestre.semestre_id}""") else: H.append("""""") if sem_locked: H.append(scu.icontag("lock32_img", title="verrouillé")) H.append("""""") if modimpl.module.is_apc(): H.append(_ue_coefs_html(modimpl)) else: H.append( f"""Coef. dans le semestre: { "non défini" if modimpl.module.coefficient is None else modimpl.module.coefficient }""" ) H.append("""
Formation: {formsemestre.formation.titre}
Inscrits: {len(mod_inscrits)} étudiants""" ) if modimpl.can_change_inscriptions(raise_exc=False): H.append( f"""modifier""" ) H.append( """
Absences dans ce module""" ) # Adapté à partir d'une suggestion de DS (Le Havre) # Liens saisies absences seulement si permission et date courante dans le semestre if ( current_user.has_permission(Permission.AbsChange) and formsemestre.est_courant() and not disable_abs ): group_id = sco_groups.get_default_group(formsemestre_id) H.append( f""" Saisie Absences """ ) current_week: str = datetime.datetime.now().strftime("%G-W%V") H.append( f""" Saisie Absences (Hebdo) """ ) H.append("
") # if not modimpl.check_apc_conformity(nt): H.append( """
Les poids des évaluations de ce module ne permettent pas d'évaluer toutes les UEs (compétences) prévues par les coefficients du programme. Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE. Vérifiez les poids des évaluations.
""" ) if not modimpl.check_apc_conformity( nt, evaluation_type=Evaluation.EVALUATION_SESSION2 ): H.append( """
Il y a des évaluations de deuxième session mais leurs poids ne permettent pas d'évaluer toutes les UEs (compétences) prévues par les coefficients du programme. La deuxième session ne sera donc pas prise en compte. Vérifiez les poids de ces évaluations.
""" ) if not modimpl.check_apc_conformity( nt, evaluation_type=Evaluation.EVALUATION_RATTRAPAGE ): H.append( """
Il y a des évaluations de rattrapage mais leurs poids n'évaluent pas toutes les UEs (compétences) prévues par les coefficients du programme. Vérifiez les poids de ces évaluations.
""" ) if formsemestre_has_decisions(formsemestre_id): H.append( """
Décisions de jury saisies: seul le ou la responsable du semestre peut saisir des notes (elle devra modifier les décisions de jury).
""" ) # H.append( f"""

{nb_evaluations} évaluations : """ ) # # Liste les noms de partitions partitions = sco_groups.get_partitions_list(formsemestre.id) H.append( """Afficher les groupes de       Voir toutes les notes

""" ) # -------- Tableau des evaluations top_table_links = "" if can_edit_evals: top_table_links = f"""Créer nouvelle évaluation """ if nb_evaluations > 0: top_table_links += f""" Trier par date """ bot_table_links = ( top_table_links + f""" Importer les notes """ ) else: bot_table_links = top_table_links if nb_evaluations > 0: H.append( '" ) H.append("""""") eval_index = nb_evaluations - 1 first_eval = True for evaluation in evaluations: H.append( _ligne_evaluation( modimpl, evaluation, first_eval=first_eval, partition_id=partition_id, arrow_down=arrow_down, arrow_none=arrow_none, arrow_up=arrow_up, can_edit_evals=can_edit_evals, can_edit_notes=can_edit_notes, eval_index=eval_index, nb_evals=nb_evaluations, is_apc=nt.is_apc, max_poids=max_poids_by_type.get(evaluation.evaluation_type, 10000.0), ) ) eval_index -= 1 first_eval = False # H.append("""
""") if sem_locked: H.append(f"""{scu.icontag("lock32_img")} semestre verrouillé""") elif can_edit_evals: H.append( f"""
{bot_table_links}
""" ) H.append( f"""
{_html_modimpl_etuds_attente(nt, modimpl)}

Légende

Rappel : seules les notes des évaluations complètement saisies (affichées en vert) apparaissent dans les bulletins.

""" ) return render_template( "sco_page.j2", content="".join(H), title=f"{mod_type_name} {module.code} {module.titre}", ) def _ligne_evaluation( modimpl: ModuleImpl, evaluation: Evaluation, first_eval: bool = True, partition_id: int = None, arrow_down=None, arrow_none=None, arrow_up=None, can_edit_evals: bool = False, can_edit_notes: bool = False, eval_index: int = 0, nb_evals: int = 0, is_apc: bool = False, max_poids: float = 0.0, ) -> str: """Ligne décrivant une évaluation dans le tableau de bord moduleimpl.""" H = [] # evaluation: Evaluation = db.session.get(Evaluation, eval_dict["evaluation_id"]) etat = sco_evaluations.do_evaluation_etat( evaluation.id, partition_id=partition_id, select_first_partition=True, ) if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE: tr_class = "mievr mievr_rattr" elif evaluation.evaluation_type == Evaluation.EVALUATION_SESSION2: tr_class = "mievr mievr_session2" elif evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: tr_class = "mievr mievr_bonus" else: tr_class = "mievr" if not evaluation.visibulletin: tr_class += " non_visible_inter" tr_class_1 = "mievr" if evaluation.is_blocked(): tr_class += " evaluation_blocked" tr_class_1 += " evaluation_blocked" if not first_eval: H.append(""" """) tr_class_1 += " mievr_spaced" H.append( f"""""" ) coef = evaluation.coefficient if is_apc: if not evaluation.get_ue_poids_dict(): # Au cas où les poids par défaut n'existent pas encore: if evaluation.set_default_poids(): db.session.commit() coef *= sum(evaluation.get_ue_poids_dict().values()) if modimpl.module.ue.type != UE_SPORT: # Avertissement si coefs x poids nuls if coef < scu.NOTES_PRECISION: if modimpl.module.module_type == scu.ModuleType.MALUS: H.append("""malus""") else: H.append("""coef. nul !""") elif is_apc: # visualisation des poids (Hinton map) H.append(_evaluation_poids_html(evaluation, max_poids)) H.append("""
""") if evaluation.date_debut: H.append(evaluation.descr_date()) else: H.append( f"""Évaluation sans date""" ) H.append(f"    {evaluation.description}") if evaluation.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE: H.append( """rattrapage""" ) elif evaluation.evaluation_type == Evaluation.EVALUATION_SESSION2: H.append( """session 2""" ) elif evaluation.evaluation_type == Evaluation.EVALUATION_BONUS: H.append( """bonus""" ) # if etat["last_modif"]: H.append( f"""(dernière modif le { etat["last_modif"].strftime(scu.DATEATIME_FMT)})""" ) # H.append( f""" { eval_index:2} """ ) # Fleches: if eval_index != (nb_evals - 1) and can_edit_evals: H.append( f"""{arrow_up}""" ) else: H.append(arrow_none) if (eval_index > 0) and can_edit_evals: H.append( f"""{arrow_down}""" ) else: H.append(arrow_none) if evaluation.is_blocked(): etat_txt = f"""évaluation bloquée { "jusqu'au " + evaluation.blocked_until.strftime(scu.DATE_FMT) if evaluation.blocked_until < Evaluation.BLOCKED_FOREVER else "" } """ etat_descr = """prise en compte bloquée""" elif etat["evalcomplete"]: etat_txt = f"""Moyenne (prise en compte{ "" if evaluation.visibulletin else ", cachée en intermédiaire"}) """ etat_descr = f"""notes utilisées dans les moyennes{ ", évaluation cachée sur les bulletins en version intermédiaire et sur la passerelle" }""" elif etat["evalattente"] and not evaluation.publish_incomplete: etat_txt = "Moyenne (prise en compte, mais notes en attente)" etat_descr = "il y a des notes en attente" elif evaluation.publish_incomplete: etat_txt = """(prise en compte immédiate)""" etat_descr = ( "il manque des notes, mais la prise en compte immédiate a été demandée" ) elif etat["nb_notes"] != 0: etat_txt = "Moyenne (non prise en compte)" etat_descr = "il manque des notes" else: etat_txt = "" if etat_txt: if can_edit_evals: etat_txt = f"""{etat_txt}""" H.append( f"""   Durée Coef. Notes Abs N {etat_txt} """ ) if can_edit_evals: H.append( f"""{scu.icontag("edit_img", alt="modifier", title="Modifier informations")}""" ) if can_edit_notes: H.append( f"""{scu.icontag("notes_img", alt="saisie notes", title="Saisie des notes")}""" ) if etat["nb_notes"] == 0: if can_edit_evals: H.append( f"""""" ) H.append(scu.icontag("delete_img", alt="supprimer", title="Supprimer")) if can_edit_evals: H.append("""""") elif etat["evalcomplete"]: H.append( f"""{scu.icontag("status_green_img", title="ok")}""" ) else: if etat["evalattente"]: H.append( f"""{scu.icontag( "status_greenorange_img", file_format="gif", title="notes en attente", )}""" ) else: H.append( f"""{scu.icontag("status_orange_img", title="il manque des notes")}""" ) # if evaluation.visibulletin: H.append( scu.icontag( "status_visible_img", title="visible dans bulletins intermédiaires" ) ) else: H.append(" ") H.append('') if can_edit_notes: H.append( moduleimpl_evaluation_menu( evaluation, nbnotes=etat["nb_notes"], ) ) # H.append( f""" {evaluation.descr_duree()} {evaluation.coefficient:g} """ ) H.append( f""" {etat["nb_notes"]} / {etat["nb_inscrits"]} {etat["nb_abs"]} {etat["nb_neutre"]} """ % etat ) if etat["nb_notes"]: H.append( f"""{etat["moy"]} / 20   (afficher)""" ) else: H.append( f"""saisir notes """ ) H.append("""""") # if etat["nb_notes"] == 0: H.append(f"""""") # if modimpl.module.is_apc(): # H.append( # f"""{ # evaluation.get_ue_poids_str()}""" # ) # else: # H.append('') H.append("""""") else: # il y a deja des notes saisies gr_moyennes = etat["gr_moyennes"] # first_group = True for gr_moyenne in gr_moyennes: H.append(f""" """) # if first_group and modimpl.module.is_apc(): # H.append( # f"""{ # evaluation.get_ue_poids_str()}""" # ) # else: H.append("""""") # first_group = False if gr_moyenne["group_name"] is None: name = "Tous" # tous else: name = f"""Groupe {gr_moyenne["group_name"]}""" H.append( f"""{name}   """ ) if gr_moyenne["gr_nb_notes"] > 0: H.append( f"""{gr_moyenne["gr_moy"]}  ({gr_moyenne["gr_nb_notes"]} notes""" ) if gr_moyenne["gr_nb_att"] > 0: H.append( f""", { gr_moyenne["gr_nb_att"]} en attente""" ) H.append(""")""") if gr_moyenne["group_id"] in etat["gr_incomplets"]: H.append("""[""") if can_edit_notes: H.append( f"""incomplet : terminer saisie]""" ) else: H.append("""incomplet]""") else: H.append("""  """) if can_edit_notes: H.append( f"""""" ) H.append("pas de notes") if can_edit_notes: H.append("""""") H.append("") H.append("""""") return "\n".join(H) def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> str: """graphe html (Hinton map) montrant les poids x coef de l'évaluation""" ue_poids = evaluation.get_ue_poids_dict(sort=True) # { ue_id : poids } if not ue_poids: return "" values = [poids * (evaluation.coefficient) for poids in ue_poids.values()] colors = [db.session.get(UniteEns, ue_id).color for ue_id in ue_poids] return _html_hinton_map( classes=("evaluation_poids",), colors=colors, max_val=max_poids, title=f"Poids de l'évaluation vers les UEs: {evaluation.get_ue_poids_str()}", values=values, ) def _html_hinton_map( classes=(), colors=(), max_val: float | None = None, size=12, title: str = "", values=(), ) -> str: """Représente une liste de nombres sous forme de carrés""" if max_val is None: max_val = max(values) if max_val < scu.NOTES_PRECISION: return "" return ( f"""
""" + "\n".join( [ f"""
""" for value, color in zip(values, colors) ] ) + "
" ) def _html_modimpl_etuds_attente(res: ResultatsSemestre, modimpl: ModuleImpl) -> str: """Affiche la liste des étudiants ayant au moins une note en attente dans ce modimpl""" m_res = res.modimpls_results.get(modimpl.id) if m_res: if not m_res.etudids_attente: return "
Aucun étudiant n'a de notes en attente.
" elif len(m_res.etudids_attente) < 10: return f"""

Étudiants avec une note en attente :

{list_etuds.html_table_etuds(m_res.etudids_attente)} """ else: return f"""
{ len(m_res.etudids_attente) } étudiants ont des notes en attente.
""" return ""