Calcul des etuds d'un modimpl avec notes en ATT. Affichage sur tableau bord. Fix tri liste etuds (#595).

This commit is contained in:
Emmanuel Viennet 2023-02-10 22:04:09 +01:00
parent 58184664df
commit 1309e77bfa
12 changed files with 299 additions and 100 deletions

View File

@ -85,6 +85,8 @@ class ModuleImplResults:
"{ evaluation.id : bool } indique si à prendre en compte ou non." "{ evaluation.id : bool } indique si à prendre en compte ou non."
self.evaluations_etat = {} self.evaluations_etat = {}
"{ evaluation_id: EvaluationEtat }" "{ evaluation_id: EvaluationEtat }"
self.etudids_attente = set()
"etudids avec au moins une note ATT dans ce module"
self.en_attente = False self.en_attente = False
"Vrai si au moins une évaluation a une note en attente" "Vrai si au moins une évaluation a une note en attente"
# #
@ -145,7 +147,6 @@ class ModuleImplResults:
evals_notes = pd.DataFrame(index=self.etudids, dtype=float) evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
self.evaluations_completes = [] self.evaluations_completes = []
self.evaluations_completes_dict = {} self.evaluations_completes_dict = {}
self.en_attente = False
for evaluation in moduleimpl.evaluations: for evaluation in moduleimpl.evaluations:
eval_df = self._load_evaluation_notes(evaluation) eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi tous les inscrits (non dem) au semestre ont une note # is_complete ssi tous les inscrits (non dem) au semestre ont une note
@ -172,15 +173,20 @@ class ModuleImplResults:
eval_df, how="left", left_index=True, right_index=True eval_df, how="left", left_index=True, right_index=True
) )
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires) # Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
nb_att = sum( eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
evals_notes[str(evaluation.id)][list(inscrits_module)] eval_etudids_attente = set(
== scu.NOTES_ATTENTE eval_notes_inscr.iloc[
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
].index
) )
self.etudids_attente |= eval_etudids_attente
self.evaluations_etat[evaluation.id] = EvaluationEtat( self.evaluations_etat[evaluation.id] = EvaluationEtat(
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete evaluation_id=evaluation.id,
nb_attente=len(eval_etudids_attente),
is_complete=is_complete,
) )
if nb_att > 0: # au moins une note en ATT dans ce modimpl:
self.en_attente = True self.en_attente = bool(self.etudids_attente)
# Force columns names to integers (evaluation ids) # Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int) evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)

View File

@ -5,10 +5,12 @@ import pandas as pd
import flask_sqlalchemy import flask_sqlalchemy
from app import db from app import db
from app.auth.models import User
from app.comp import df_cache from app.comp import df_cache
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.modules import Module from app.models.modules import Module
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -99,6 +101,27 @@ class ModuleImpl(db.Model):
d.pop("module", None) d.pop("module", None)
return d return d
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
"""Check if user can modify module resp.
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
= Admin, et dir des etud. (si option l'y autorise)
"""
if not self.formsemestre.etat:
if raise_exc:
raise ScoLockedSemError("Modification impossible: semestre verrouille")
return False
# -- check access
# admin ou resp. semestre avec flag resp_can_change_resp
if user.has_permission(Permission.ScoImplement):
return True
if (
user.id in [resp.id for resp in self.formsemestre.responsables]
) and self.formsemestre.resp_can_change_ens:
return True
if raise_exc:
raise AccessDenied(f"Modification impossible pour {user}")
return False
# Enseignants (chargés de TD ou TP) d'un moduleimpl # Enseignants (chargés de TD ou TP) d'un moduleimpl
notes_modules_enseignants = db.Table( notes_modules_enseignants = db.Table(

View File

@ -122,6 +122,14 @@ class ScoLockedFormError(ScoValueError):
super().__init__(msg=msg, dest_url=dest_url) super().__init__(msg=msg, dest_url=dest_url)
class ScoLockedSemError(ScoValueError):
"Modification d'un formsemestre verrouillé"
def __init__(self, msg="", dest_url=None):
msg = "Ce semestre est verrouillé ! " + str(msg)
super().__init__(msg=msg, dest_url=dest_url)
class ScoNonEmptyFormationObject(ScoValueError): class ScoNonEmptyFormationObject(ScoValueError):
"""On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent""" """On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent"""

View File

@ -377,7 +377,7 @@ def can_change_module_resp(moduleimpl_id):
if not current_user.has_permission(Permission.ScoImplement) and ( if not current_user.has_permission(Permission.ScoImplement) and (
(current_user.id not in sem["responsables"]) or (not sem["resp_can_change_ens"]) (current_user.id not in sem["responsables"]) or (not sem["resp_can_change_ens"])
): ):
raise AccessDenied("Modification impossible pour %s" % current_user) raise AccessDenied(f"Modification impossible pour {current_user}")
return M, sem return M, sem

View File

@ -38,6 +38,7 @@ from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
from app import log from app import log
from app.tables import list_etuds
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
@ -520,14 +521,15 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
H.append(f"""<th title="{ue.titre or ''}">{ue.acronyme}</th>""") H.append(f"""<th title="{ue.titre or ''}">{ue.acronyme}</th>""")
H.append("""</tr>""") H.append("""</tr>""")
for etudid, ues_etud in table_inscr.items(): etuds = list_etuds.etuds_sorted_from_ids(table_inscr.keys())
etud: Identite = Identite.query.get(etudid) for etud in etuds:
ues_etud = table_inscr[etud.id]
H.append( H.append(
f"""<tr><td><a class="discretelink etudinfo" id={etudid} f"""<tr><td><a class="discretelink etudinfo" id={etud.id}
href="{url_for( href="{url_for(
"scolar.ficheEtud", "scolar.ficheEtud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etudid, etudid=etud.id,
)}" )}"
>{etud.nomprenom}</a></td>""" >{etud.nomprenom}</a></td>"""
) )
@ -539,7 +541,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
else: else:
# Validations d'UE déjà enregistrées dans d'autres semestres # Validations d'UE déjà enregistrées dans d'autres semestres
validations_ue = ( validations_ue = (
ScolarFormSemestreValidation.query.filter_by(etudid=etudid) ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.filter( .filter(
ScolarFormSemestreValidation.formsemestre_id ScolarFormSemestreValidation.formsemestre_id
!= res.formsemestre.id, != res.formsemestre.id,
@ -556,7 +558,8 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
) )
validation = validations_ue[-1] if validations_ue else None validation = validations_ue[-1] if validations_ue else None
expl_validation = ( expl_validation = (
f"""Validée ({validation.code}) le {validation.event_date.strftime("%d/%m/%Y")}""" f"""Validée ({validation.code}) le {
validation.event_date.strftime("%d/%m/%Y")}"""
if validation if validation
else "" else ""
) )
@ -567,13 +570,13 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
title="{etud.nomprenom} {'inscrit' if est_inscr else 'non inscrit'} à l'UE {ue.acronyme}. {expl_validation}", title="{etud.nomprenom} {'inscrit' if est_inscr else 'non inscrit'} à l'UE {ue.acronyme}. {expl_validation}",
onchange="change_ue_inscr(this);" onchange="change_ue_inscr(this);"
data-url_inscr={ data-url_inscr={
url_for("notes.etud_inscrit_ue", url_for("notes.etud_inscrit_ue",
scodoc_dept=g.scodoc_dept, etudid=etudid, scodoc_dept=g.scodoc_dept, etudid=etud.id,
formsemestre_id=res.formsemestre.id, ue_id=ue.id) formsemestre_id=res.formsemestre.id, ue_id=ue.id)
} }
data-url_desinscr={ data-url_desinscr={
url_for("notes.etud_desinscrit_ue", url_for("notes.etud_desinscrit_ue",
scodoc_dept=g.scodoc_dept, etudid=etudid, scodoc_dept=g.scodoc_dept, etudid=etud.id,
formsemestre_id=res.formsemestre.id, ue_id=ue.id) formsemestre_id=res.formsemestre.id, ue_id=ue.id)
} }
/> />

View File

@ -36,6 +36,7 @@ from flask_login import current_user
from app import db from app import db
from app.auth.models import User from app.auth.models import User
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_common import ResultatsSemestre
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, ModuleImpl from app.models import FormSemestre, ModuleImpl
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation
@ -59,9 +60,7 @@ from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_users from app.tables import list_etuds
# ported from old DTML code in oct 2009
# menu evaluation dans moduleimpl # menu evaluation dans moduleimpl
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
@ -196,23 +195,20 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if not isinstance(moduleimpl_id, int): if not isinstance(moduleimpl_id, int):
raise ScoInvalidIdType("moduleimpl_id must be an integer !") raise ScoInvalidIdType("moduleimpl_id must be an integer !")
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
M = modimpl.to_dict() mi_dict = modimpl.to_dict()
formsemestre_id = modimpl.formsemestre_id formsemestre_id = modimpl.formsemestre_id
formsemestre: FormSemestre = modimpl.formsemestre formsemestre: FormSemestre = modimpl.formsemestre
Mod = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0] mod_dict = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] formation_dict = sco_formations.formation_list(
args={"formation_id": sem["formation_id"]}
)[0]
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
moduleimpl_id=moduleimpl_id moduleimpl_id=moduleimpl_id
) )
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# Evaluations, la plus RECENTE en tête
# mod_evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
# mod_evals.sort(
# key=lambda x: (x["numero"], x["jour"], x["heure_debut"]), reverse=True
# )
# la plus RECENTE en tête
evaluations = modimpl.evaluations.order_by( evaluations = modimpl.evaluations.order_by(
Evaluation.numero.desc(), Evaluation.numero.desc(),
Evaluation.jour.desc(), Evaluation.jour.desc(),
@ -240,18 +236,23 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
) )
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags() arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
# #
module_resp = User.query.get(M["responsable_id"]) module_resp = User.query.get(modimpl.responsable_id)
mod_type_name = scu.MODULE_TYPE_NAMES[Mod["module_type"]] mod_type_name = scu.MODULE_TYPE_NAMES[mod_dict["module_type"]]
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title=f"{mod_type_name} {Mod['code']} {Mod['titre']}" page_title=f"{mod_type_name} {mod_dict['code']} {mod_dict['titre']}",
javascripts=["js/etud_info.js"],
init_qtip=True,
), ),
f"""<h2 class="formsemestre">{mod_type_name} f"""<h2 class="formsemestre">{mod_type_name}
<tt>{Mod['code']}</tt> {Mod['titre']} <tt>{mod_dict['code']}</tt> {mod_dict['titre']}
{"dans l'UE " + modimpl.module.ue.acronyme if modimpl.module.module_type == scu.ModuleType.MALUS else ""} {"dans l'UE " + modimpl.module.ue.acronyme
if modimpl.module.module_type == scu.ModuleType.MALUS
else ""
}
</h2> </h2>
<div class="moduleimpl_tableaubord moduleimpl_type_{ <div class="moduleimpl_tableaubord moduleimpl_type_{
scu.ModuleType(Mod['module_type']).name.lower()}"> scu.ModuleType(mod_dict['module_type']).name.lower()}">
<table> <table>
<tr> <tr>
<td class="fichetitre2">Responsable: </td><td class="redboldtext"> <td class="fichetitre2">Responsable: </td><td class="redboldtext">
@ -259,18 +260,14 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
<span class="blacktt">({module_resp.user_name})</span> <span class="blacktt">({module_resp.user_name})</span>
""", """,
] ]
try: if modimpl.can_change_ens_by(current_user):
sco_moduleimpl.can_change_module_resp(moduleimpl_id)
H.append( H.append(
"""<a class="stdlink" href="edit_moduleimpl_resp?moduleimpl_id=%s">modifier</a>""" f"""<a class="stdlink" href="{url_for("notes.edit_moduleimpl_resp",
% moduleimpl_id scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
}" >modifier</a>"""
) )
except:
pass
H.append("""</td><td>""") H.append("""</td><td>""")
H.append( H.append(", ".join([u.get_nomprenom() for u in modimpl.enseignants]))
", ".join([sco_users.user_info(m["ens_id"])["nomprenom"] for m in M["ens"]])
)
H.append("""</td><td>""") H.append("""</td><td>""")
try: try:
sco_moduleimpl.can_change_ens(moduleimpl_id) sco_moduleimpl.can_change_ens(moduleimpl_id)
@ -302,7 +299,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
H.append("""</td><td></td></tr>""") H.append("""</td><td></td></tr>""")
# 3ieme ligne: Formation # 3ieme ligne: Formation
H.append( H.append(
"""<tr><td class="fichetitre2">Formation: </td><td>%(titre)s</td></tr>""" % F """<tr><td class="fichetitre2">Formation: </td><td>%(titre)s</td></tr>"""
% formation_dict
) )
# Ligne: Inscrits # Ligne: Inscrits
H.append( H.append(
@ -312,15 +310,18 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if current_user.has_permission(Permission.ScoEtudInscrit): if current_user.has_permission(Permission.ScoEtudInscrit):
H.append( H.append(
"""<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">modifier</a>""" """<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">modifier</a>"""
% M["moduleimpl_id"] % mi_dict["moduleimpl_id"]
) )
H.append("</td></tr>") H.append("</td></tr>")
# Ligne: règle de calcul # Ligne: règle de calcul
has_expression = sco_compute_moy.moduleimpl_has_expression(M) has_expression = sco_compute_moy.moduleimpl_has_expression(mi_dict)
if has_expression: if has_expression:
H.append( H.append(
'<tr><td class="fichetitre2" colspan="4">Règle de calcul: <span class="formula" title="mode de calcul de la moyenne du module">moyenne=<tt>%s</tt></span>' f"""<tr>
% M["computation_expr"] <td class="fichetitre2" colspan="4">Règle de calcul:
<span class="formula" title="mode de calcul de la moyenne du module"
>moyenne=<tt>{mi_dict["computation_expr"]}</tt>
</span>"""
) )
H.append("""<span class="warning">inutilisée dans cette version de ScoDoc""") H.append("""<span class="warning">inutilisée dans cette version de ScoDoc""")
if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False): if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
@ -380,20 +381,24 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
# #
if formsemestre_has_decisions(formsemestre_id): if formsemestre_has_decisions(formsemestre_id):
H.append( H.append(
"""<ul class="tf-msg"><li class="tf-msg warning">Décisions de jury saisies: seul le responsable du semestre peut saisir des notes (il devra modifier les décisions de jury).</li></ul>""" """<ul class="tf-msg">
<li class="tf-msg warning">Décisions de jury saisies: seul le responsable du
semestre peut saisir des notes (il devra modifier les décisions de jury).
</li>
</ul>"""
) )
# #
H.append( H.append(
"""<p><form name="f"><span style="font-size:120%%; font-weight: bold;">%d évaluations :</span> f"""<p><form name="f">
<span style="font-size:120%%; font-weight: bold;">{nb_evaluations} évaluations :</span>
<span style="padding-left: 30px;"> <span style="padding-left: 30px;">
<input type="hidden" name="moduleimpl_id" value="%s"/>""" <input type="hidden" name="moduleimpl_id" value="{moduleimpl_id}"/>"""
% (nb_evaluations, moduleimpl_id)
) )
# #
# Liste les noms de partitions # Liste les noms de partitions
partitions = sco_groups.get_partitions_list(sem["formsemestre_id"]) partitions = sco_groups.get_partitions_list(sem["formsemestre_id"])
H.append( H.append(
"""Afficher les groupes """Afficher les groupes
de&nbsp;<select name="partition_id" onchange="document.f.submit();">""" de&nbsp;<select name="partition_id" onchange="document.f.submit();">"""
) )
been_selected = False been_selected = False
@ -409,8 +414,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if name is None: if name is None:
name = "Tous" name = "Tous"
H.append( H.append(
"""<option value="%s" %s>%s</option>""" f"""<option value="{partition['partition_id']}" {selected}>{name}</option>"""
% (partition["partition_id"], selected, name)
) )
H.append( H.append(
"""</select> """</select>
@ -420,20 +424,21 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
</form> </form>
</p> </p>
""" """
% M % mi_dict
) )
# -------- Tableau des evaluations # -------- Tableau des evaluations
top_table_links = "" top_table_links = ""
if can_edit_evals: if can_edit_evals:
top_table_links = f"""<a class="stdlink" href="{ top_table_links = f"""<a class="stdlink" href="{
url_for("notes.evaluation_create", scodoc_dept=g.scodoc_dept, moduleimpl_id=M['moduleimpl_id']) url_for("notes.evaluation_create", scodoc_dept=g.scodoc_dept, moduleimpl_id=mi_dict['moduleimpl_id'])
}">Créer nouvelle évaluation</a> }">Créer nouvelle évaluation</a>
""" """
if nb_evaluations > 0: if nb_evaluations > 0:
top_table_links += f""" top_table_links += f"""
<a class="stdlink" style="margin-left:2em;" href="{ <a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.moduleimpl_evaluation_renumber", scodoc_dept=g.scodoc_dept, moduleimpl_id=M['moduleimpl_id'], url_for("notes.moduleimpl_evaluation_renumber",
scodoc_dept=g.scodoc_dept, moduleimpl_id=mi_dict['moduleimpl_id'],
redirect=1) redirect=1)
}">Trier par date</a> }">Trier par date</a>
""" """
@ -477,31 +482,35 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
f"""</td></tr> f"""</td></tr>
</table> </table>
<div class="list_etuds_attente">
{_html_modimpl_etuds_attente(nt, modimpl)}
</div>
</div> </div>
<!-- LEGENDE --> <!-- LEGENDE -->
<hr> <hr>
<h4>Légende</h4> <h4>Légende</h4>
<ul> <ul>
<li>{scu.icontag("edit_img")} : modifie description de l'évaluation <li>{scu.icontag("edit_img")} : modifie description de l'évaluation
(date, heure, coefficient, ...) (date, heure, coefficient, ...)
</li> </li>
<li>{scu.icontag("notes_img")} : saisie des notes</li> <li>{scu.icontag("notes_img")} : saisie des notes</li>
<li>{scu.icontag("delete_img")} : indique qu'il n'y a aucune note <li>{scu.icontag("delete_img")} : indique qu'il n'y a aucune note
entrée (cliquer pour supprimer cette évaluation) entrée (cliquer pour supprimer cette évaluation)
</li> </li>
<li>{scu.icontag("status_orange_img")} : indique qu'il manque <li>{scu.icontag("status_orange_img")} : indique qu'il manque
quelques notes dans cette évaluation quelques notes dans cette évaluation
</li> </li>
<li>{scu.icontag("status_green_img")} : toutes les notes sont <li>{scu.icontag("status_green_img")} : toutes les notes sont
entrées (cliquer pour les afficher) entrées (cliquer pour les afficher)
</li> </li>
<li>{scu.icontag("status_visible_img")} : indique que cette évaluation <li>{scu.icontag("status_visible_img")} : indique que cette évaluation
sera mentionnée dans les bulletins au format "intermédiaire" sera mentionnée dans les bulletins au format "intermédiaire"
</li> </li>
</ul> </ul>
<p>Rappel : seules les notes des évaluations complètement saisies <p>Rappel : seules les notes des évaluations complètement saisies
(affichées en vert) apparaissent dans les bulletins. (affichées en vert) apparaissent dans les bulletins.
</p> </p>
""" """
@ -844,3 +853,22 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
+ "</div>" + "</div>"
) )
return H return H
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 "<div><em>Aucun étudiant n'a de notes en attente.</em></div>"
elif len(m_res.etudids_attente) < 10:
return f"""
<h4>Étudiants avec une note en attente&nbsp;:</h4>
{list_etuds.html_table_etuds(m_res.etudids_attente)}
"""
else:
return f"""<div class="warning"><em>{
len(m_res.etudids_attente)
} étudiants ont des notes en attente.</em></div>"""
return ""

View File

@ -641,4 +641,9 @@ table.dataTable.order-column.stripe.hover tbody tr.even:hover td.sorting_1 {
table.dataTable.gt_table { table.dataTable.gt_table {
width: auto; width: auto;
padding-right: 5px; padding-right: 5px;
}
/* Tables non centrées */
table.dataTable.gt_table.gt_left {
margin-left: 16px;
} }

117
app/tables/list_etuds.py Normal file
View File

@ -0,0 +1,117 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Liste simple d'étudiants
"""
from flask import g, url_for
from app.models import Identite
from app.tables import table_builder as tb
class TableEtud(tb.Table):
"""Table listant des étudiants
Peut-être sous-classée pour ajouter des colonnes.
L'id de la ligne est etuid, et le row stocke etud.
"""
def __init__(
self,
etuds: list[Identite] = None,
classes: list[str] = None,
row_class=None,
with_foot_titles=False,
**kwargs,
):
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
classes = classes or ["gt_table", "gt_left"]
super().__init__(
row_class=row_class or RowEtud,
classes=classes,
with_foot_titles=with_foot_titles,
**kwargs,
)
self.add_etuds(etuds)
def add_etuds(self, etuds: list[Identite]):
"Ajoute des étudiants à la table"
for etud in etuds:
row = self.row_class(self, etud)
row.add_etud_cols()
self.add_row(row)
class RowEtud(tb.Row):
"Ligne de la table d'étudiants"
# pour le moment très simple, extensible (codes, liens bulletins, ...)
def __init__(self, table: TableEtud, etud: Identite, *args, **kwargs):
super().__init__(table, etud.id, *args, **kwargs)
self.etud = etud
def add_etud_cols(self):
"""Ajoute colonnes étudiant: codes, noms"""
etud = self.etud
self.table.group_titles.update(
{
"etud_codes": "Codes",
"identite_detail": "",
"identite_court": "",
}
)
# --- Codes (seront cachés, mais exportés en excel)
# self.add_cell("etudid", "etudid", etud.id, "etud_codes")
# self.add_cell(
# "code_nip",
# "code_nip",
# etud.code_nip or "",
# "etud_codes",
# )
# --- Identité étudiant
# url_bulletin = url_for(
# "notes.formsemestre_bulletinetud",
# scodoc_dept=g.scodoc_dept,
# formsemestre_id=res.formsemestre.id,
# etudid=etud.id,
# )
url_bulletin = None # pour extension future
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
self.add_cell(
"nom_disp",
"Nom",
etud.nom_disp(),
"identite_detail",
data={"order": etud.sort_key},
target=url_bulletin,
target_attrs={"class": "etudinfo discretelink", "id": str(etud.id)},
)
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
# self.add_cell(
# "nom_short",
# "Nom",
# etud.nom_short,
# "identite_court",
# data={
# "order": etud.sort_key,
# "etudid": etud.id,
# "nomprenom": etud.nomprenom,
# },
# target=url_bulletin,
# target_attrs={"class": "etudinfo", "id": str(etud.id)},
# )
def etuds_sorted_from_ids(etudids) -> list[Identite]:
"Liste triée d'etuds à partir d'une collections d'etudids"
etuds = [Identite.query.get_or_404(etudid) for etudid in etudids]
return sorted(etuds, key=lambda etud: etud.sort_key)
def html_table_etuds(etudids) -> str:
"""Table HTML simple des étudiants indiqués"""
etuds = etuds_sorted_from_ids(etudids)
table = TableEtud(etuds)
return table.html()

View File

@ -38,8 +38,6 @@ class TableRecap(tb.Table):
moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE
On ajoute aussi des classes: On ajoute aussi des classes:
- pour les lignes:
selected_row pour l'étudiant sélectionné
- les colonnes: - les colonnes:
- la moyenne générale a la classe col_moy_gen - la moyenne générale a la classe col_moy_gen
- les colonnes SAE ont la classe col_sae - les colonnes SAE ont la classe col_sae

View File

@ -68,6 +68,7 @@ class Table(Element):
classes: list[str] = None, classes: list[str] = None,
attrs: dict[str, str] = None, attrs: dict[str, str] = None,
data: dict = None, data: dict = None,
with_foot_titles=True,
row_class=None, row_class=None,
xls_sheet_name="feuille", xls_sheet_name="feuille",
xls_before_table=[], # liste de cellules a placer avant la table xls_before_table=[], # liste de cellules a placer avant la table
@ -100,8 +101,10 @@ class Table(Element):
self.head_title_row: "Row" = Row( self.head_title_row: "Row" = Row(
self, "title_head", cell_elt="th", classes=["titles"] self, "title_head", cell_elt="th", classes=["titles"]
) )
self.foot_title_row: "Row" = Row( self.foot_title_row: "Row" = (
self, "title_foot", cell_elt="th", classes=["titles"] Row(self, "title_foot", cell_elt="th", classes=["titles"])
if with_foot_titles
else None
) )
self.empty_cell = Cell.empty() self.empty_cell = Cell.empty()
# Excel (xls) spécifique: # Excel (xls) spécifique:
@ -119,8 +122,10 @@ class Table(Element):
""" """
self.sort_columns() self.sort_columns()
# Titres # Titres
self.add_head_row(self.head_title_row) if self.head_title_row:
self.add_foot_row(self.foot_title_row) self.add_head_row(self.head_title_row)
if self.foot_title_row:
self.add_foot_row(self.foot_title_row)
def get_row_by_id(self, row_id) -> "Row": def get_row_by_id(self, row_id) -> "Row":
"return the row, or None" "return the row, or None"
@ -261,18 +266,23 @@ class Table(Element):
title = title or "" title = title or ""
if col_id not in self.titles: if col_id not in self.titles:
self.titles[col_id] = title self.titles[col_id] = title
self.head_title_row.cells[col_id] = self.head_title_row.add_cell( if self.head_title_row:
col_id, self.head_title_row.cells[col_id] = self.head_title_row.add_cell(
None, col_id,
title, None,
classes=classes, title,
group=self.column_group.get(col_id), classes=classes,
) group=self.column_group.get(col_id),
self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell( )
col_id, None, title, classes=classes if self.foot_title_row:
) self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell(
col_id, None, title, classes=classes
return self.head_title_row.cells.get(col_id), self.foot_title_row.cells[col_id] )
head_cell = (
self.head_title_row.cells.get(col_id) if self.head_title_row else None
)
foot_cell = self.foot_title_row.cells[col_id] if self.foot_title_row else None
return head_cell, foot_cell
def excel(self, wb: Workbook = None): def excel(self, wb: Workbook = None):
"""Simple Excel representation of the table.""" """Simple Excel representation of the table."""

View File

@ -1043,15 +1043,18 @@ def edit_enseignants_form(moduleimpl_id):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
def edit_moduleimpl_resp(moduleimpl_id): def edit_moduleimpl_resp(moduleimpl_id: int):
"""Changement d'un enseignant responsable de module """Changement d'un enseignant responsable de module
Accessible par Admin et dir des etud si flag resp_can_change_ens Accessible par Admin et dir des etud si flag resp_can_change_ens
""" """
M, sem = sco_moduleimpl.can_change_module_resp(moduleimpl_id) modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
modimpl.can_change_ens_by(current_user, raise_exc=True) # access control
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
'Modification du responsable du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>' f"""Modification du responsable du <a href="{
% (moduleimpl_id, M["module"]["titre"]), url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
}">module {modimpl.module.titre or ""}</a>""",
javascripts=["libjs/AutoSuggest.js"], javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"], cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')", bodyOnLoad="init_tf_form('')",
@ -1065,9 +1068,9 @@ def edit_moduleimpl_resp(moduleimpl_id):
uid2display[u["id"]] = u["nomplogin"] uid2display[u["id"]] = u["nomplogin"]
allowed_user_names = list(uid2display.values()) allowed_user_names = list(uid2display.values())
initvalues = M initvalues = modimpl.to_dict(with_module=False)
initvalues["responsable_id"] = uid2display.get( initvalues["responsable_id"] = uid2display.get(
M["responsable_id"], M["responsable_id"] modimpl.responsable_id, modimpl.responsable_id
) )
form = [ form = [
("moduleimpl_id", {"input_type": "hidden"}), ("moduleimpl_id", {"input_type": "hidden"}),
@ -1112,9 +1115,8 @@ def edit_moduleimpl_resp(moduleimpl_id):
) )
else: else:
responsable_id = User.get_user_id_from_nomplogin(tf[2]["responsable_id"]) responsable_id = User.get_user_id_from_nomplogin(tf[2]["responsable_id"])
if ( if not responsable_id:
not responsable_id # presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps)
): # presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps)
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
@ -1123,16 +1125,15 @@ def edit_moduleimpl_resp(moduleimpl_id):
) )
) )
sco_moduleimpl.do_moduleimpl_edit( modimpl.responsable_id = responsable_id
{"moduleimpl_id": moduleimpl_id, "responsable_id": responsable_id}, db.session.add(modimpl)
formsemestre_id=sem["formsemestre_id"], db.session.commit()
) flash("Responsable modifié")
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
head_message="responsable modifié",
) )
) )

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.4.39" SCOVERSION = "9.4.40"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"