diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index dfd051c1..fdf6aaad 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -14,7 +14,6 @@ from app import log from app.comp import moy_sem from app.comp.aux_stats import StatsMoyenne from app.comp.res_common import ResultatsSemestre -from app.comp import res_sem from app.models import FormSemestre from app.models import Identite from app.models import ModuleImpl diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 805744af..04ed7dce 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -5,7 +5,9 @@ import datetime from app import db +from app.models.etudiants import Identite from app.models.moduleimpls import ModuleImpl +from app.models.notes import NotesNotes from app.models.ues import UniteEns from app.scodoc.sco_exceptions import ScoValueError @@ -176,6 +178,12 @@ class Evaluation(db.Model): ] ) + def get_etud_note(self, etud: Identite) -> NotesNotes: + """La note de l'étudiant, ou None si pas noté. + (nb: pas de cache, lent, ne pas utiliser pour des calculs) + """ + return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first() + class EvaluationUEPoids(db.Model): """Poids des évaluations (BUT) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 5742da15..f376ef74 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -27,13 +27,14 @@ from app.models.but_refcomp import ( ApcReferentielCompetences, ) from app.models.groups import GroupDescr, Partition +from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu from app.models.but_refcomp import parcours_formsemestre from app.models.etudiants import Identite from app.models.formations import Formation from app.models.modules import Module -from app.models.moduleimpls import ModuleImpl +from app.models.moduleimpls import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation @@ -690,6 +691,31 @@ class FormSemestre(db.Model): ) return "\n".join(H) + def etud_set_all_missing_notes(self, etud: Identite, value=None) -> int: + """Met toutes les notes manquantes de cet étudiant dans ce semestre + (ie dans toutes les évaluations des modules auxquels il est inscrit et n'a pas de note) + à la valeur donnée par value, qui est en général "ATT", "ABS", "EXC". + """ + from app.scodoc import sco_saisie_notes + + inscriptions = ( + ModuleImplInscription.query.filter_by(etudid=etud.id) + .join(ModuleImpl) + .filter_by(formsemestre_id=self.id) + ) + nb_recorded = 0 + for inscription in inscriptions: + for evaluation in inscription.modimpl.evaluations: + if evaluation.get_etud_note(etud) is None: + if not sco_saisie_notes.do_evaluation_set_etud_note( + evaluation, etud, value + ): + raise ScoValueError( + "erreur lors de l'enregistrement de la note" + ) + nb_recorded += 1 + return nb_recorded + # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre notes_formsemestre_responsables = db.Table( diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index f9ff9e52..24f3d6e1 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -29,16 +29,19 @@ """ import datetime + from flask import current_app from flask import g from flask import request -from flask import render_template, url_for +from flask import flash, redirect, render_template, url_for from flask_login import current_user - +import pandas as pd from app import log 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, Module +from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType @@ -605,7 +608,9 @@ def formsemestre_description_table( """Description du semestre sous forme de table exportable Liste des modules et de leurs coefficients """ - formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + formsemestre: FormSemestre = FormSemestre.query.filter_by( + id=formsemestre_id, dept_id=g.scodoc_dept_id + ).first_or_404() 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})[ @@ -1063,6 +1068,7 @@ def formsemestre_status(formsemestre_id=None): formsemestre_status_head( formsemestre_id=formsemestre_id, page_title="Tableau de bord" ), + formsemestre_warning_etuds_sans_note(formsemestre, nt), """

Tableau de bord: cliquez sur un module pour saisir des notes

""", @@ -1326,3 +1332,135 @@ def formsemestre_tableau_modules( H.append("") return "\n".join(H) + + +# Expérimental +def get_formsemestre_etudids_sans_notes( + formsemestre: FormSemestre, res: ResultatsSemestre +) -> set[int]: + """Les étudis d'étudiants de ce semestre n'ayant aucune note + alors que d'autres en ont. + """ + # Il y a-t-il des notes prises en compte ? + # On regarde la moy. gen., qui pour les étudiants sans notes est NaN en classique + # ou nulle en APC. + if all(res.etud_moy_gen.eq(0.0, fill_value=0.0)): + return set() # tout est 0 ou NaN, empty set + etudids_sans_notes = set.intersection( + *[ + set.intersection(*m_res.evals_etudids_sans_note.values()) + for m_res in res.modimpls_results.values() + if m_res.evals_etudids_sans_note + ] + ) + nb_sans_notes = len(etudids_sans_notes) + if nb_sans_notes > 0 and nb_sans_notes < len( + formsemestre.get_inscrits(include_demdef=False) + ): + return etudids_sans_notes + return set() + + +def formsemestre_warning_etuds_sans_note( + formsemestre: FormSemestre, res: ResultatsSemestre +) -> str: + """Vérifie si on est dans la situation où certains (mais pas tous) étudiants + n'ont aucune note alors que d'autres en ont. + Ce cas se produit typiquement quand on inscrit un étudiant en cours de semestre. + Il est alors utile de proposer de mettre toutes ses notes à ABS, ATT ou EXC + pour éviter de laisser toutes les évaluations "incomplètes". + """ + etudids_sans_notes = get_formsemestre_etudids_sans_notes(formsemestre, res) + if not etudids_sans_notes: + return "" + nb_sans_notes = len(etudids_sans_notes) + if nb_sans_notes < 5: + # peu d'étudiants, affiche leurs noms + etuds: list[Identite] = sorted( + [Identite.query.get(etudid) for etudid in etudids_sans_notes], + key=lambda e: e.sort_key, + ) + noms = ", ".join( + [ + f"""{etud.nomprenom}""" + for etud in etuds + ] + ) + msg_etuds = ( + f"""{noms} n'{"a" if nb_sans_notes == 1 else "ont"} aucune note :""" + ) + else: + msg_etuds = f"""{nb_sans_notes} étudiants n'ont aucune note :""" + + return f"""
Attention: {msg_etuds} + {"lui" if nb_sans_notes == 1 else "leur"} + affecter des notes. +
+ """ + + +def formsemestre_note_etuds_sans_notes(formsemestre_id: int, code: str = None): + """Vue affichant les étudiants sans notes""" + formsemestre: FormSemestre = FormSemestre.query.filter_by( + id=formsemestre_id, dept_id=g.scodoc_dept_id + ).first_or_404() + res: ResultatsSemestre = res_sem.load_formsemestre_results(formsemestre) + etudids_sans_notes = get_formsemestre_etudids_sans_notes(formsemestre, res) + etuds: list[Identite] = sorted( + [Identite.query.get(etudid) for etudid in etudids_sans_notes], + key=lambda e: e.sort_key, + ) + if request.method == "POST": + if not code in ("ATT", "EXC", "ABS"): + raise ScoValueError("code invalide: doit être ATT, ABS ou EXC") + for etud in etuds: + formsemestre.etud_set_all_missing_notes(etud, code) + flash(f"Notes de {len(etuds)} étudiants affectées à {code}") + return redirect( + url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + ) + ) + noms = "
  • ".join( + [ + f"""{etud.nomprenom}""" + for etud in etuds + ] + ) + return f""" + {html_sco_header.sco_header( + page_title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}" + )} +
    + {formsemestre_status_head( + formsemestre_id=formsemestre_id, page_title="Étudiants sans notes" + )} +
    +

    Étudiants sans notes:

    + + +
    + + Mettre toutes les notes de ces étudiants à : + + +
    + {html_sco_header.sco_footer()} + """ diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index d110f3ba..5e114677 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -40,6 +40,7 @@ from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import Evaluation, FormSemestre from app.models import ScolarNews +from app.models.etudiants import Identite import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb @@ -300,6 +301,25 @@ def do_evaluation_upload_xls(): return 0, msg + "

    (pas de notes modifiées)

    " +def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -> bool: + """Enregistre la note d'un seul étudiant + value: valeur externe (float ou str) + """ + if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl.id): + raise AccessDenied(f"Modification des notes impossible pour {current_user}") + # Convert and check value + L, invalids, _, _, _ = _check_notes( + [(etud.id, value)], evaluation.to_dict(), evaluation.moduleimpl.module.to_dict() + ) + if len(invalids) == 0: + nb_changed, _, _ = notes_add( + current_user, evaluation.id, L, "Initialisation notes" + ) + if nb_changed == 1: + return True + return False # error + + def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): """Initialisation des notes manquantes""" evaluation = Evaluation.query.get_or_404(evaluation_id) @@ -318,7 +338,7 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): for etudid, _ in etudid_etats: # pour tous les inscrits if etudid not in notes_db: # pas de note notes.append((etudid, value)) - # Check value + # Convert and check values L, invalids, _, _, _ = _check_notes( notes, evaluation.to_dict(), modimpl.module.to_dict() ) diff --git a/app/views/notes.py b/app/views/notes.py index 5b4dd416..28d1852a 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -203,6 +203,12 @@ sco_publish( Permission.ScoImplement, methods=["GET", "POST"], ) +sco_publish( + "/formsemestre_view_etuds_sans_note", + sco_formsemestre_status.formsemestre_note_etuds_sans_notes, + Permission.ScoView, + methods=["GET", "POST"], +) sco_publish( "/formsemestre_recapcomplet", sco_recapcomplet.formsemestre_recapcomplet,