# -*- 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"""
"""
# 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"""
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(""" |
""")
# 2ieme ligne: Semestre, Coef
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(""" | |
""")
# 3ieme ligne: Formation
H.append(
f"""
Formation: | {formsemestre.formation.titre} |
"""
)
# Ligne: Inscrits
H.append(
f"""Inscrits: | {len(mod_inscrits)} étudiants"""
)
if modimpl.can_change_inscriptions(raise_exc=False):
H.append(
f"""modifier"""
)
H.append(
""" |
|
"""
)
disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre)
if not disable_abs:
H.append(
f"""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(
"""
"""
)
#
H.append(
f"""
"""
)
# -------- 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(
'
'
+ top_table_links
+ "
"
)
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
- {scu.icontag("edit_img")} : modifie description de l'évaluation
(date, heure, coefficient, ...)
- {scu.icontag("notes_img")} : saisie des notes
- {scu.icontag("delete_img")} : indique qu'il n'y a aucune note
entrée (cliquer pour supprimer cette évaluation)
- {scu.icontag("status_orange_img")} : indique qu'il manque
quelques notes dans cette évaluation
- {scu.icontag("status_green_img")} : toutes les notes sont
entrées (cliquer pour les afficher)
- {scu.icontag("status_visible_img")} : indique que cette évaluation
sera mentionnée dans les bulletins au format "intermédiaire"
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(' |
{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 ""