ScoDoc-PE/app/scodoc/sco_formsemestre_status.py

1329 lines
49 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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 render_template, url_for
from flask_login import current_user
from app import log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Evaluation, Module
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 sco_codes_parcours
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 parcours",
"endpoint": "notes.formsemestre_graph_parcours",
"args": {"formsemestre_id": formsemestre_id},
"enabled": True,
},
{
"title": "Codes des parcours",
"endpoint": "notes.formsemestre_suivi_parcours",
"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": {
"formation_id": formation.id,
"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_change_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},
"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": "Voir 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": "Créer/modifier les 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": "Modifier les groupes", "submenu": submenu, "enabled": enabled}
)
menu_groupes.append(
{
"title": "Expérimental: éditeur de partitions",
"endpoint": "scolar.partition_editor",
"args": {"formsemestre_id": formsemestre_id},
"enabled": sco_groups.sco_permissions_check.can_change_groups(
formsemestre_id
),
"helpmsg": "Une spécialité de Mulhouse",
},
)
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": "Saisie des notes",
"endpoint": "notes.formsemestre_status",
"args": {"formsemestre_id": formsemestre_id},
"enabled": True,
"helpmsg": "Tableau de bord du semestre",
},
{
"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": "Générer feuille préparation Jury",
"endpoint": "notes.feuille_preparation_jury",
"args": {"formsemestre_id": formsemestre_id},
},
{
"title": "Saisie des décisions du jury",
"endpoint": "notes.formsemestre_saisie_jury",
"args": {
"formsemestre_id": formsemestre_id,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
},
{
"title": "Éditer les PV et archiver les résultats",
"endpoint": "notes.formsemestre_archive",
"args": {"formsemestre_id": formsemestre_id},
"enabled": sco_permissions_check.can_edit_pv(formsemestre_id),
},
{
"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 = [
'<ul id="sco_menu">',
htmlutils.make_menu("Semestre", menu_semestre),
htmlutils.make_menu("Inscriptions", menu_inscriptions),
htmlutils.make_menu("Groupes", menu_groupes),
htmlutils.make_menu("Notes", menu_notes),
htmlutils.make_menu("Jury", menu_jury),
htmlutils.make_menu("Statistiques", menu_stats),
formsemestre_custommenu_html(formsemestre_id),
"</ul>",
]
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 (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.query.get_or_404(formsemestre_id)
h = render_template(
"formsemestre_page_title.html",
formsemestre=formsemestre,
scu=scu,
sem_menu_bar=formsemestre_status_menubar(formsemestre),
)
return h
def fill_formsemestre(sem):
"""Add some useful fields to help display formsemestres"""
sem["notes_url"] = scu.NotesURL()
formsemestre_id = sem["formsemestre_id"]
if not sem["etat"]:
sem[
"locklink"
] = f"""<a href="{url_for('notes.formsemestre_change_lock',
scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id )
}">{scu.icontag("lock_img", border="0", title="Semestre verrouillé")}</a>"""
else:
sem["locklink"] = ""
if sco_preferences.get_preference("bul_display_publication", formsemestre_id):
if sem["bul_hide_xml"]:
eyeicon = scu.icontag("hide_img", border="0", title="Bulletins NON publiés")
else:
eyeicon = scu.icontag("eye_img", border="0", title="Bulletins publiés")
sem[
"eyelink"
] = f"""<a href="{
url_for('notes.formsemestre_change_publication_bul',
scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id)
}">{eyeicon}</a>"""
else:
sem["eyelink"] = ""
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
sem["formation"] = F
parcours = sco_codes_parcours.get_parcours_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.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": formsemestre.formation_id})[
0
]
parcours = sco_codes_parcours.get_parcours_from_code(F["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.query_ues().all()
columns_ids += [f"ue_{ue.id}" for ue in ues]
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
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"] = "Evaluation"
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()}"
R = []
sum_coef = 0
sum_ects = 0
last_ue_id = None
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,
"ects": ects_str,
"Module": ue.titre,
"_css_row_class": "table_row_ue",
"_UE_td_attrs": f'style="background-color: {ue.color} !important;"'
if ue.color
else "",
}
if use_ue_coefs:
ue_info["Coef."] = ue.coefficient
ue_info["Coef._class"] = "ue_coef"
R.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)
l = {
"UE": modimpl.module.ue.acronyme,
"_UE_td_attrs": ue_info["_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:
l[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
if with_parcours:
l["parcours"] = ", ".join(
sorted([pa.code for pa in modimpl.module.parcours])
)
R.append(l)
if with_evals:
# Ajoute lignes pour evaluations
evals = nt.get_mod_evaluation_etat_list(modimpl.id)
evals.reverse() # ordre chronologique
# Ajoute etat:
for e in evals:
e["UE"] = l["UE"]
e["_UE_td_attrs"] = l["_UE_td_attrs"]
e["Code"] = l["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"
R += evals
sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef}
R.append(sums)
return GenTable(
columns_ids=columns_ids,
rows=R,
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="%s?formsemestre_id=%s&with_evals=%s"
% (request.base_url, formsemestre_id, 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"""
<form name="f" method="get" action="{request.base_url}">
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()"
{ "checked" if with_evals else "" }
>indiquer les évaluations</input>
<input type="checkbox" name="with_parcours" value="1" onchange="document.f.submit()"
{ "checked" if with_parcours else "" }
>indiquer les parcours BUT</input>
"""
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"""<h3>Listes de {formsemestre.titre}
<span class="infostitresem">({formsemestre.mois_debut()} - {formsemestre.mois_fin()})</span></h3>"""
)
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"""
<td>
<a href="%(url_etat)s">absences</a>
</td>
<td>
<form action="{url_for(
"absences.SignaleAbsenceGrSemestre", scodoc_dept=g.scodoc_dept
)}" method="get">
<input type="hidden" name="datefin" value="{
formsemestre.date_fin.strftime("%d/%m/%Y")}"/>
<input type="hidden" name="group_ids" value="%(group_id)s"/>
<input type="hidden" name="destination" value="{destination}"/>
<input type="submit" value="Saisir abs des" />
<select name="datedebut" class="noprint">
"""
date = first_monday
for idx, jour in enumerate(sco_abs.day_names()):
form_abs_tmpl += f"""<option value="{date}" {
'selected' if idx == weekday else ''
}>{jour}s</option>"""
date = date.next_day()
form_abs_tmpl += f"""
</select>
<a href="{
url_for("absences.choix_semaine", scodoc_dept=g.scodoc_dept)
}?group_id=%(group_id)s">saisie par semaine</a>
</form></td>
"""
else:
form_abs_tmpl = ""
except ScoInvalidDateError: # dates incorrectes dans semestres ?
form_abs_tmpl = ""
#
H.append('<div id="grouplists">')
# 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("<h4>Tous les étudiants</h4>")
else:
H.append("<h4>Groupes de %(partition_name)s</h4>" % partition)
groups = sco_groups.get_partition_groups(partition)
if groups:
H.append("<table>")
for group in groups:
n_members = len(sco_groups.get_group_members(group["group_id"]))
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"""
<tr class="listegroupelink">
<td>
<a href="{
url_for("scolar.groups_view",
group_ids=group["group_id"],
scodoc_dept=g.scodoc_dept,
)
}">{group["label"]}</a>
</td><td>
</td>
<td>({n_members} étudiants)</td>
"""
)
if with_absences:
H.append(form_abs_tmpl % group)
H.append("</tr>")
H.append("</table>")
else:
H.append('<p class="help indent">Aucun groupe dans cette partition')
if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id):
H.append(
f""" (<a href="{url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=partition["partition_id"])
}" class="stdlink">créer</a>)"""
)
H.append("</p>")
if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id):
H.append(
f"""<h4><a
href="{
url_for("scolar.edit_partition_form",
formsemestre_id=formsemestre.id,
scodoc_dept=g.scodoc_dept,
)
}">Ajouter une partition</a></h4>"""
)
H.append("</div>")
return "\n".join(H)
def html_expr_diagnostic(diagnostics):
"""Affiche messages d'erreur des formules utilisateurs"""
H = []
H.append('<div class="ue_warning">Erreur dans des formules utilisateurs:<ul>')
last_id, last_msg = None, None
for diag in diagnostics:
if "moduleimpl_id" in diag:
mod = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=diag["moduleimpl_id"]
)[0]
H.append(
'<li>module <a href="moduleimpl_status?moduleimpl_id=%s">%s</a>: %s</li>'
% (
diag["moduleimpl_id"],
mod["module"]["abbrev"] or mod["module"]["code"] or "?",
diag["msg"],
)
)
else:
if diag["ue_id"] != last_id or diag["msg"] != last_msg:
ue = sco_edit_ue.ue_list({"ue_id": diag["ue_id"]})[0]
H.append(
'<li>UE "%s": %s</li>'
% (ue["acronyme"] or ue["titre"] or "?", diag["msg"])
)
last_id, last_msg = diag["ue_id"], diag["msg"]
H.append("</ul></div>")
return "".join(H)
def formsemestre_status_head(formsemestre_id=None, page_title=None):
"""En-tête HTML des pages "semestre" """
sem = FormSemestre.query.get(formsemestre_id)
if not sem:
raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)")
formation = sem.formation
parcours = formation.get_parcours()
page_title = page_title or "Modules de "
H = [
html_sco_header.html_sem_header(
page_title, with_page_header=False, with_h2=False
),
f"""<table>
<tr><td class="fichetitre2">Formation: </td><td>
<a href="{url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=sem.formation.id)}"
class="discretelink" title="Formation {
formation.acronyme}, v{formation.version}">{formation.titre}</a>
""",
]
if sem.semestre_id >= 0:
H.append(", %s %s" % (parcours.SESSION_NAME, sem.semestre_id))
if sem.modalite:
H.append(f"&nbsp;en {sem.modalite}")
if sem.etapes:
H.append(
f"""&nbsp;&nbsp;&nbsp;(étape <b><tt>{
sem.etapes_apo_str() or "-"
}</tt></b>)"""
)
H.append("</td></tr>")
if sem.parcours:
H.append(
f"""
<tr><td class="fichetitre2">Parcours: </td>
<td style="color: blue;">{', '.join(parcours.code for parcours in sem.parcours)}</td>
</tr>
"""
)
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id)
H.append(
'<tr><td class="fichetitre2">Évaluations: </td><td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides'
% evals
)
if evals["last_modif"]:
H.append(
" <em>(dernière note saisie le %s)</em>"
% evals["last_modif"].strftime("%d/%m/%Y à %Hh%M")
)
H.append("</td></tr>")
if evals["attente"]:
H.append(
"""<tr><td class="fichetitre2"></td><td class="redboldtext">
Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur indicative.
</td></tr>"""
)
H.append("</table>")
sem_warning = ""
if sem.bul_hide_xml:
sem_warning += "Bulletins non publiés sur le portail. "
if sem.block_moyennes:
sem_warning += "Calcul des moyennes bloqué !"
if sem_warning:
H.append('<p class="fontorange"><em>' + sem_warning + "</em></p>")
if sem.semestre_id >= 0 and not sem.est_sur_une_annee():
H.append(
'<p class="fontorange"><em>Attention: ce semestre couvre plusieurs années scolaires !</em></p>'
)
return "".join(H)
def formsemestre_status(formsemestre_id=None):
"""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)
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)
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()}"
),
'<div class="formsemestre_status">',
formsemestre_status_head(
formsemestre_id=formsemestre_id, page_title="Tableau de bord"
),
"""<p><b style="font-size: 130%">Tableau de bord: </b>
<span class="help">cliquez sur un module pour saisir des notes</span>
</p>""",
]
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"""
<div class="tableau_modules">
{_TABLEAU_MODULES_HEAD}
<tr class="formsemestre_status_cat">
<td colspan="5">
<span class="status_module_cat">Ressources</span>
</td>
</tr>
{formsemestre_tableau_modules(
ressources, nt, formsemestre_id, can_edit=can_edit, show_ues=False
)}
<tr class="formsemestre_status_cat">
<td colspan="5">
<span class="status_module_cat">SAÉs</span>
</td>
</tr>""",
formsemestre_tableau_modules(
saes, nt, formsemestre_id, can_edit=can_edit, show_ues=False
),
]
if autres:
H += [
"""<tr class="formsemestre_status_cat">
<td colspan="5">
<span class="status_module_cat">Autres modules</span>
</td></tr>""",
formsemestre_tableau_modules(
autres, nt, formsemestre_id, can_edit=can_edit, show_ues=False
),
]
H += [_TABLEAU_MODULES_FOOT, "</div>"]
else:
# formations classiques: groupe par UE
H += [
"<p>",
_TABLEAU_MODULES_HEAD,
formsemestre_tableau_modules(
modimpls,
nt,
formsemestre_id,
can_edit=can_edit,
use_ue_coefs=use_ue_coefs,
),
_TABLEAU_MODULES_FOOT,
"</p>",
]
if use_ue_coefs and not formsemestre.formation.is_apc():
H.append(
"""
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
"""
)
# --- LISTE DES ETUDIANTS
H += [
'<div id="groupes">',
_make_listes_sem(formsemestre),
"</div>",
]
# --- Lien mail enseignants:
adrlist = list(mails_enseignants - {None, ""})
if adrlist:
H.append(
'<p><a class="stdlink" href="mailto:?cc=%s">Courrier aux %d enseignants du semestre</a></p>'
% (",".join(adrlist), len(adrlist))
)
return "".join(H) + html_sco_header.sco_footer()
_TABLEAU_MODULES_HEAD = """
<table class="formsemestre_status">
<tr>
<th class="formsemestre_status">Code</th>
<th class="formsemestre_status">Module</th>
<th class="formsemestre_status">Inscrits</th>
<th class="resp">Responsable</th>
<th class="coef">Coefs.</th>
<th class="evals">Évaluations</th>
</tr>
"""
_TABLEAU_MODULES_FOOT = """</table>"""
def formsemestre_tableau_modules(
modimpls: list[dict],
nt,
formsemestre_id: int,
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"])
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 += " <b>(coef. %s)</b>" % (ue["coefficient"] or 0.0)
H.append(
f"""<tr class="formsemestre_status_ue"><td colspan="4">
<span class="status_ue_acro">{ue["acronyme"]}</span>
<span class="status_ue_title">{titre}</span>
</td><td colspan="2">"""
)
expr = sco_compute_moy.get_ue_expression(
formsemestre_id, ue["ue_id"], html_quote=True
)
if expr:
H.append(
f""" <span class="formula" title="mode de calcul de la moyenne d'UE">{expr}</span>
<span class="warning">formule inutilisée en 9.2: <a href="{
url_for("notes.delete_ue_expr", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ue_id=ue["ue_id"] )
}
">supprimer</a></span>"""
)
H.append("</td></tr>")
if modimpl["ue"]["type"] != sco_codes_parcours.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'<tr class="formsemestre_status_green{fontorange}">')
else:
H.append(f'<tr class="formsemestre_status{fontorange}">')
H.append(
f"""<td class="formsemestre_status_code""><a
href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl['moduleimpl_id'])}"
title="{mod_descr}" class="stdlink">{mod.code}</a></td>"""
)
H.append(
f"""<td class="scotext"><a href="{
url_for( "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl["moduleimpl_id"]
) }" title="{mod_descr}" class="formsemestre_status_link">{mod.abbrev or mod.titre or ""}</a>
</td>
<td class="formsemestre_status_inscrits">{len(mod_inscrits)}</td>
<td class="resp scotext">
<a class="discretelink" href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl["moduleimpl_id"]
) }" title="{mod_ens}">{ sco_users.user_info(modimpl["responsable_id"])["prenomnom"] }</a>
</td>
<td>
"""
)
if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE):
coefs = mod.ue_coefs_list()
H.append(f'<a class="invisible_link" href="#" title="{mod_descr}">')
for coef in coefs:
if coef[1] > 0:
H.append(
f"""<span class="mod_coef_indicator"
title="{coef[0].acronyme}"
style="background: {
coef[0].color if coef[0].color is not None else 'blue'
}"></span>"""
)
else:
H.append("""<span class="mod_coef_indicator_zero"></span>""")
H.append("</a>")
H.append("</td>")
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('<td class="evals">')
nb_evals = (
etat["nb_evals_completes"]
+ etat["nb_evals_en_cours"]
+ etat["nb_evals_vides"]
)
if nb_evals != 0:
H.append(
'<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">%s prévues, %s ok</a>'
% (modimpl["moduleimpl_id"], nb_evals, etat["nb_evals_completes"])
)
if etat["nb_evals_en_cours"] > 0:
H.append(
', <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il manque des notes">%s en cours</a></span>'
% (modimpl["moduleimpl_id"], etat["nb_evals_en_cours"])
)
if etat["attente"]:
H.append(
' <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il y a des notes en attente">[en attente]</a></span>'
% modimpl["moduleimpl_id"]
)
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(
"""<td class="malus">
<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">malus (%d notes)</a>
"""
% (modimpl["moduleimpl_id"], nb_malus_notes)
)
else:
raise ValueError(f"Invalid module_type {mod.module_type}") # a bug
H.append("</td></tr>")
return "\n".join(H)