Compare commits

...

9 Commits

24 changed files with 687 additions and 678 deletions

View File

@ -374,114 +374,115 @@ def formsemestre_etudiants(
return sorted(etuds, key=itemgetter("sort_key"))
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etat_evals(formsemestre_id: int):
"""
Informations sur l'état des évaluations d'un formsemestre.
# retrait (temporaire ? à discuter)
# @bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
# @api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
# @login_required
# @scodoc
# @permission_required(Permission.ScoView)
# @as_json
# def etat_evals(formsemestre_id: int):
# """
# Informations sur l'état des évaluations d'un formsemestre.
formsemestre_id : l'id d'un semestre
# formsemestre_id : l'id d'un semestre
Exemple de résultat :
[
{
"id": 1, // moduleimpl_id
"titre": "Initiation aux réseaux informatiques",
"evaluations": [
{
"id": 1,
"description": null,
"datetime_epreuve": null,
"heure_fin": "09:00:00",
"coefficient": "02.00"
"is_complete": true,
"nb_inscrits": 16,
"nb_manquantes": 0,
"ABS": 0,
"ATT": 0,
"EXC": 0,
"saisie_notes": {
"datetime_debut": "2021-09-11T00:00:00+02:00",
"datetime_fin": "2022-08-25T00:00:00+02:00",
"datetime_mediane": "2022-03-19T00:00:00+01:00"
}
},
...
]
},
]
"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# Exemple de résultat :
# [
# {
# "id": 1, // moduleimpl_id
# "titre": "Initiation aux réseaux informatiques",
# "evaluations": [
# {
# "id": 1,
# "description": null,
# "datetime_epreuve": null,
# "heure_fin": "09:00:00",
# "coefficient": "02.00"
# "is_complete": true,
# "nb_inscrits": 16,
# "nb_manquantes": 0,
# "ABS": 0,
# "ATT": 0,
# "EXC": 0,
# "saisie_notes": {
# "datetime_debut": "2021-09-11T00:00:00+02:00",
# "datetime_fin": "2022-08-25T00:00:00+02:00",
# "datetime_mediane": "2022-03-19T00:00:00+01:00"
# }
# },
# ...
# ]
# },
# ]
# """
# query = FormSemestre.query.filter_by(id=formsemestre_id)
# if g.scodoc_dept:
# query = query.filter_by(dept_id=g.scodoc_dept_id)
# formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
# app.set_sco_dept(formsemestre.departement.acronym)
# nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
result = []
for modimpl_id in nt.modimpls_results:
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
modimpl_dict = modimpl.to_dict(convert_objects=True)
# result = []
# for modimpl_id in nt.modimpls_results:
# modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
# modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
# modimpl_dict = modimpl.to_dict(convert_objects=True)
list_eval = []
for evaluation_id in modimpl_results.evaluations_etat:
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
evaluation = Evaluation.query.get_or_404(evaluation_id)
eval_dict = evaluation.to_dict_api()
eval_dict["etat"] = eval_etat.to_dict()
# list_eval = []
# for evaluation_id in modimpl_results.evaluations_etat:
# eval_etat = modimpl_results.evaluations_etat[evaluation_id]
# evaluation = Evaluation.query.get_or_404(evaluation_id)
# eval_dict = evaluation.to_dict_api()
# eval_dict["etat"] = eval_etat.to_dict()
eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
eval_dict["nb_notes_manquantes"] = len(
modimpl_results.evals_etudids_sans_note[evaluation.id]
)
eval_dict["nb_notes_abs"] = sum(
modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE
)
eval_dict["nb_notes_att"] = eval_etat.nb_attente
eval_dict["nb_notes_exc"] = sum(
modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE
)
# eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
# eval_dict["nb_notes_manquantes"] = len(
# modimpl_results.evals_etudids_sans_note[evaluation.id]
# )
# eval_dict["nb_notes_abs"] = sum(
# modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE
# )
# eval_dict["nb_notes_att"] = eval_etat.nb_attente
# eval_dict["nb_notes_exc"] = sum(
# modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE
# )
# Récupération de toutes les notes de l'évaluation
# eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id)
# # Récupération de toutes les notes de l'évaluation
# # eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id)
notes = NotesNotes.query.filter_by(evaluation_id=evaluation.id).all()
# notes = NotesNotes.query.filter_by(evaluation_id=evaluation.id).all()
date_debut = None
date_fin = None
date_mediane = None
# date_debut = None
# date_fin = None
# date_mediane = None
# Si il y a plus d'une note saisie pour l'évaluation
if len(notes) >= 1:
# Tri des notes en fonction de leurs dates
notes_sorted = sorted(notes, key=attrgetter("date"))
# # Si il y a plus d'une note saisie pour l'évaluation
# if len(notes) >= 1:
# # Tri des notes en fonction de leurs dates
# notes_sorted = sorted(notes, key=attrgetter("date"))
date_debut = notes_sorted[0].date
date_fin = notes_sorted[-1].date
# date_debut = notes_sorted[0].date
# date_fin = notes_sorted[-1].date
# Note médiane
date_mediane = notes_sorted[len(notes_sorted) // 2].date
# # Note médiane
# date_mediane = notes_sorted[len(notes_sorted) // 2].date
eval_dict["saisie_notes"] = {
"datetime_debut": date_debut.isoformat()
if date_debut is not None
else None,
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
"datetime_mediane": date_mediane.isoformat()
if date_mediane is not None
else None,
}
# eval_dict["saisie_notes"] = {
# "datetime_debut": date_debut.isoformat()
# if date_debut is not None
# else None,
# "datetime_fin": date_fin.isoformat() if date_fin is not None else None,
# "datetime_mediane": date_mediane.isoformat()
# if date_mediane is not None
# else None,
# }
list_eval.append(eval_dict)
# list_eval.append(eval_dict)
modimpl_dict["evaluations"] = list_eval
result.append(modimpl_dict)
return result
# modimpl_dict["evaluations"] = list_eval
# result.append(modimpl_dict)
# return result
@bp.route("/formsemestre/<int:formsemestre_id>/resultats")

View File

@ -417,7 +417,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
+ '</div><div class="warning">'.join(messages)
+ "</div>"
)
self.codes = [self.codes[0]] + sorted(self.codes[1:])
self.codes = [self.codes[0]] + sorted((c or "") for c in self.codes[1:])
def passage_de_droit_en_but3(self) -> tuple[bool, str]:
"""Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""

View File

@ -56,6 +56,7 @@ class EvaluationEtat:
evaluation_id: int
nb_attente: int
nb_notes: int # nb notes d'étudiants inscrits au semestre et au modimpl
is_complete: bool
def to_dict(self):
@ -168,13 +169,15 @@ class ModuleImplResults:
# NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
# Ce merge ne garde que les étudiants inscrits au module
# et met à NULL les notes non présentes
# et met à NULL (NaN) les notes non présentes
# (notes non saisies ou etuds non inscrits au module):
evals_notes = evals_notes.merge(
eval_df, how="left", left_index=True, right_index=True
)
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
nb_notes = eval_notes_inscr.notna().sum()
eval_etudids_attente = set(
eval_notes_inscr.iloc[
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
@ -184,6 +187,7 @@ class ModuleImplResults:
self.evaluations_etat[evaluation.id] = EvaluationEtat(
evaluation_id=evaluation.id,
nb_attente=len(eval_etudids_attente),
nb_notes=nb_notes,
is_complete=is_complete,
)
# au moins une note en ATT dans ce modimpl:

View File

@ -9,12 +9,13 @@
from collections import Counter, defaultdict
from collections.abc import Generator
import datetime
from functools import cached_property
from operator import attrgetter
import numpy as np
import pandas as pd
import sqlalchemy as sa
from flask import g, url_for
from app import db
@ -22,14 +23,19 @@ from app.comp import res_sem
from app.comp.res_cache import ResultatsCache
from app.comp.jury import ValidationsSemestre
from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, FormSemestreUECoef
from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models import ScolarAutorisationInscription
from app.models.ues import UniteEns
from app.models import (
Evaluation,
FormSemestre,
FormSemestreUECoef,
Identite,
ModuleImpl,
ModuleImplInscription,
ScolarAutorisationInscription,
UniteEns,
)
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_exceptions import ScoValueError, ScoTemporaryError
from app.scodoc import sco_utils as scu
@ -192,16 +198,82 @@ class ResultatsSemestre(ResultatsCache):
*[mr.etudids_attente for mr in self.modimpls_results.values()]
)
# # Etat des évaluations
# # (se substitue à do_evaluation_etat, sans les moyennes par groupes)
# def get_evaluations_etats(evaluation_id: int) -> dict:
# """Renvoie dict avec les clés:
# last_modif
# nb_evals_completes
# nb_evals_en_cours
# nb_evals_vides
# attente
# """
# Etat des évaluations
def get_evaluation_etat(self, evaluation: Evaluation) -> dict:
"""État d'une évaluation
{
"coefficient" : float, # 0 si None
"description" : str, # de l'évaluation, "" si None
"etat" {
"evalcomplete" : bool,
"last_modif" : datetime.datetime | None, # saisie de note la plus récente
"nb_notes" : int, # nb notes d'étudiants inscrits
},
"evaluatiuon_id" : int,
"jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1)
"publish_incomplete" : bool,
}
"""
mod_results = self.modimpls_results.get(evaluation.moduleimpl_id)
if mod_results is None:
raise ScoTemporaryError() # argh !
etat = mod_results.evaluations_etat.get(evaluation.id)
if etat is None:
raise ScoTemporaryError() # argh !
# Date de dernière saisie de note
cursor = db.session.execute(
sa.text(
"SELECT MAX(date) FROM notes_notes WHERE evaluation_id = :evaluation_id"
),
{"evaluation_id": evaluation.id},
)
date_modif = cursor.one_or_none()
last_modif = date_modif[0] if date_modif else None
return {
"coefficient": evaluation.coefficient or 0.0,
"description": evaluation.description or "",
"evaluation_id": evaluation.id,
"jour": evaluation.date_debut or datetime.datetime(1900, 1, 1),
"etat": {
"evalcomplete": etat.is_complete,
"nb_notes": etat.nb_notes,
"last_modif": last_modif,
},
"publish_incomplete": evaluation.publish_incomplete,
}
def get_mod_evaluation_etat_list(self, modimpl: ModuleImpl) -> list[dict]:
"""Liste des états des évaluations de ce module
[ evaluation_etat, ... ] (voir get_evaluation_etat)
trié par (numero desc, date_debut desc)
"""
# nouvelle version 2024-02-02
return list(
reversed(
[
self.get_evaluation_etat(evaluation)
for evaluation in modimpl.evaluations
]
)
)
# modernisation de get_mod_evaluation_etat_list
# utilisé par:
# sco_evaluations.do_evaluation_etat_in_mod
# e["etat"]["evalcomplete"]
# e["etat"]["nb_notes"]
# e["etat"]["last_modif"]
#
# sco_formsemestre_status.formsemestre_description_table
# "jour" (qui est e.date_debut or datetime.date(1900, 1, 1))
# "description"
# "coefficient"
# e["etat"]["evalcomplete"]
# publish_incomplete
#
# sco_formsemestre_status.formsemestre_tableau_modules
# e["etat"]["nb_notes"]
#
# --- JURY...
def get_formsemestre_validations(self) -> ValidationsSemestre:

View File

@ -423,30 +423,37 @@ class NotesTableCompat(ResultatsSemestre):
)
return evaluations
def get_evaluations_etats(self) -> list[dict]:
"""Liste de toutes les évaluations du semestre
[ {...evaluation et son etat...} ]"""
# TODO: à moderniser (voir dans ResultatsSemestre)
# utilisé par
# do_evaluation_etat_in_sem
def get_evaluations_etats(self) -> dict[int, dict]:
""" "état" de chaque évaluation du semestre
{
evaluation_id : {
"evalcomplete" : bool,
"last_modif" : datetime | None
"nb_notes" : int,
}, ...
}
"""
# utilisé par do_evaluation_etat_in_sem
evaluations_etats = {}
for modimpl in self.formsemestre.modimpls_sorted:
for evaluation in modimpl.evaluations:
evaluation_etat = self.get_evaluation_etat(evaluation)
evaluations_etats[evaluation.id] = evaluation_etat["etat"]
return evaluations_etats
from app.scodoc import sco_evaluations
if not hasattr(self, "_evaluations_etats"):
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
self.formsemestre.id
)
return self._evaluations_etats
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
"""Liste des états des évaluations de ce module"""
# XXX TODO à moderniser: lent, recharge des données que l'on a déjà...
return [
e
for e in self.get_evaluations_etats()
if e["moduleimpl_id"] == moduleimpl_id
]
# ancienne version < 2024-02-02
# def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
# """Liste des états des évaluations de ce module
# ordonnée selon (numero desc, date_debut desc)
# """
# # à moderniser: lent, recharge des données que l'on a déjà...
# # remplacemé par ResultatsSemestre.get_mod_evaluation_etat_list
# #
# return [
# e
# for e in self.get_evaluations_etats()
# if e["moduleimpl_id"] == moduleimpl_id
# ]
def get_moduleimpls_attente(self):
"""Liste des modimpls du semestre ayant des notes en attente"""

View File

@ -19,7 +19,7 @@ from app.models.departements import Departement
from app.models.scolar_event import ScolarEvent
from app.scodoc import notesdb as ndb
from app.scodoc.sco_bac import Baccalaureat
from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
from app.scodoc.sco_exceptions import ScoGenError, ScoInvalidParamError, ScoValueError
import app.scodoc.sco_utils as scu
@ -233,6 +233,15 @@ class Identite(models.ScoDocModel):
log(f"Identite.create {etud}")
return etud
def from_dict(self, args, **kwargs) -> bool:
"""Check arguments, then modify.
Add to session but don't commit.
True if modification.
"""
check_etud_duplicate_code(args, "code_nip")
check_etud_duplicate_code(args, "code_ine")
return super().from_dict(args, **kwargs)
@classmethod
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded."""
@ -728,6 +737,60 @@ class Identite(models.ScoDocModel):
)
def check_etud_duplicate_code(args, code_name, edit=True):
"""Vérifie que le code n'est pas dupliqué.
Raises ScoGenError si problème.
"""
etudid = args.get("etudid", None)
if not args.get(code_name, None):
return
etuds = Identite.query.filter_by(
**{code_name: str(args[code_name]), "dept_id": g.scodoc_dept_id}
).all()
duplicate = False
if edit:
duplicate = (len(etuds) > 1) or (
(len(etuds) == 1) and etuds[0].id != args["etudid"]
)
else:
duplicate = len(etuds) > 0
if duplicate:
listh = [] # liste des doubles
for etud in etuds:
listh.append(f"Autre étudiant: {etud.html_link_fiche()}")
if etudid:
submit_label = "retour à la fiche étudiant"
dest_endpoint = "scolar.fiche_etud"
parameters = {"etudid": etudid}
else:
if "tf_submitted" in args:
del args["tf_submitted"]
submit_label = "Continuer"
dest_endpoint = "scolar.etudident_create_form"
parameters = args
else:
submit_label = "Annuler"
dest_endpoint = "notes.index_html"
parameters = {}
err_page = f"""<h3><h3>Code étudiant ({code_name}) dupliqué !</h3>
<p class="help">Le {code_name} {args[code_name]} est déjà utilisé: un seul étudiant peut avoir
ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
</p>
<ul><li>
{ '</li><li>'.join(listh) }
</li></ul>
<p>
<a href="{ url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
">{submit_label}</a>
</p>
"""
log(f"*** error: code {code_name} duplique: {args[code_name]}")
raise ScoGenError(err_page)
def make_etud_args(
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
) -> dict:

View File

@ -349,8 +349,8 @@ class EtudiantsJuryPE:
trajectoire = trajectoire_aggr[aggregat]
if trajectoire:
# Le semestre terminal de l'étudiant de l'aggrégat
fid = trajectoire.semestre_final.formsemestre_id
formsemestres_terminaux[fid] = trajectoire.semestre_final
fid = trajectoire.formsemestre_final.formsemestre_id
formsemestres_terminaux[fid] = trajectoire.formsemestre_final
return formsemestres_terminaux
def get_formsemestres(self, semestres_recherches=None):

View File

@ -1,5 +1,5 @@
from app.comp import moy_sem
from app.pe.pe_tabletags import TableTag
from app.pe.pe_tabletags import TableTag, MoyenneTag
from app.pe.pe_etudiant import EtudiantsJuryPE
from app.pe.pe_trajectoire import Trajectoire, TrajectoiresJuryPE
from app.pe.pe_trajectoiretag import TrajectoireTag
@ -10,7 +10,6 @@ import numpy as np
class AggregatInterclasseTag(TableTag):
# -------------------------------------------------------------------------------------------------------------------
def __init__(
self,
@ -30,24 +29,30 @@ class AggregatInterclasseTag(TableTag):
"""
TableTag.__init__(self)
# Le nom
self.aggregat = nom_aggregat
"""Aggrégat de l'interclassement"""
self.nom = self.get_repr()
"""Les étudiants diplômés et leurs trajectoires (cf. trajectoires.suivis)""" # TODO
self.diplomes_ids = etudiants.etudiants_diplomes
self.etudiants_diplomes = {etudid for etudid in self.diplomes_ids}
# pour les exports sous forme de dataFrame
self.etudiants = {etudid: etudiants.identites[etudid].etat_civil for etudid in self.diplomes_ids}
self.etudiants = {
etudid: etudiants.identites[etudid].etat_civil
for etudid in self.diplomes_ids
}
# Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat
self.trajectoires: dict[int, Trajectoire] = {}
"""Ensemble des trajectoires associées à l'aggrégat"""
for trajectoire_id in trajectoires_jury_pe.trajectoires:
trajectoire = trajectoires_jury_pe.trajectoires[trajectoire_id]
if trajectoire_id[0] == nom_aggregat:
self.trajectoires[trajectoire_id] = trajectoire
self.trajectoires_taggues: dict[int, Trajectoire] = {}
"""Ensemble des trajectoires tagguées associées à l'aggrégat"""
for trajectoire_id in self.trajectoires:
self.trajectoires_taggues[trajectoire_id] = trajectoires_taggues[
trajectoire_id
@ -56,26 +61,27 @@ class AggregatInterclasseTag(TableTag):
# Les trajectoires suivies par les étudiants du jury, en ne gardant que
# celles associées aux diplomés
self.suivi: dict[int, Trajectoire] = {}
"""Association entre chaque étudiant et la trajectoire tagguée à prendre en
compte pour l'aggrégat"""
for etudid in self.diplomes_ids:
self.suivi[etudid] = trajectoires_jury_pe.suivi[etudid][nom_aggregat]
self.tags_sorted = self.do_taglist()
"""Liste des tags (triés par ordre alphabétique)"""
# Construit la matrice de notes
self.notes = self.compute_notes_matrice()
"""Matrice des notes de l'aggrégat"""
# Synthétise les moyennes/classements par tag
self.moyennes_tags = {}
self.moyennes_tags: dict[str, MoyenneTag] = {}
for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag]
self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag)
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
# Est significatif ? (aka a-t-il des tags et des notes)
self.significatif = len(self.tags_sorted) > 0
def get_repr(self) -> str:
"""Une représentation textuelle"""
return f"Aggrégat {self.aggregat}"
@ -118,7 +124,4 @@ class AggregatInterclasseTag(TableTag):
etudids_communs, tags_communs
]
# Force les nan
df.fillna(np.nan)
return df

View File

@ -65,35 +65,15 @@ import pandas as pd
class JuryPE(object):
"""Classe mémorisant toutes les informations nécessaires pour établir un jury de PE.
Modèle basé sur NotesTable.
Attributs :
* diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
* juryEtudDict : dictionnaire récapitulant les étudiants participant au jury PE (données administratives +
celles des semestres valides à prendre en compte permettant le calcul des moyennes ...
``{'etudid : { 'nom', 'prenom', 'civilite', 'diplome', '', }}``
a
Rq: il contient à la fois les étudiants qui vont être diplomés à la date prévue
et ceux qui sont éliminés (abandon, redoublement, ...) pour affichage alternatif
"""
# Variables de classe décrivant les aggrégats, leur ordre d'apparition temporelle et
# leur affichage dans les avis latex
# ------------------------------------------------------------------------------------------------------------------
def __init__(self, diplome):
"""
Création d'une table PE sur la base d'un semestre selectionné. De ce semestre est déduit :
Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base
d'une année de diplôme. De ce semestre est déduit :
1. l'année d'obtention du DUT,
2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés.
Args:
sem_base: le FormSemestre donnant le semestre à la base du jury PE
semBase: le dictionnaire sem donnant la base du jury (CB: TODO: A supprimer à long term)
meme_programme: si True, impose un même programme pour tous les étudiants participant au jury,
si False, permet des programmes differents
diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
"""
self.diplome = diplome
"L'année du diplome"
@ -101,7 +81,7 @@ class JuryPE(object):
self.nom_export_zip = f"Jury_PE_{self.diplome}"
"Nom du zip où ranger les fichiers générés"
# Chargement des étudiants à prendre en compte dans le jury
# Chargement des étudiants à prendre en compte Sydans le jury
pe_affichage.pe_print(
f"""*** Recherche et chargement des étudiants diplômés en {
self.diplome}"""
@ -122,7 +102,6 @@ class JuryPE(object):
self._gen_xls_synthese_jury_par_tag(zipfile)
self._gen_xls_synthese_par_etudiant(zipfile)
# Fin !!!! Tada :)
def _gen_xls_diplomes(self, zipfile: ZipFile):
@ -356,7 +335,9 @@ class JuryPE(object):
for aggregat in aggregats:
# Le dictionnaire par défaut des moyennes
donnees[etudid] |= get_defaut_dict_synthese_aggregat(aggregat, self.diplome)
donnees[etudid] |= get_defaut_dict_synthese_aggregat(
aggregat, self.diplome
)
# La trajectoire de l'étudiant sur l'aggrégat
trajectoire = self.trajectoires.suivi[etudid][aggregat]
@ -364,16 +345,17 @@ class JuryPE(object):
trajectoire_tagguee = self.trajectoires_tagguees[
trajectoire.trajectoire_id
]
else:
trajectoire_tagguee = None
# L'interclassement
interclass = self.interclassements_taggues[aggregat]
# Injection des données dans un dictionnaire
donnees[etudid] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome)
if tag in trajectoire_tagguee.moyennes_tags:
# La trajectoire tagguée
moy_trajectoire_tag = trajectoire_tagguee.moyennes_tags[tag]
if moy_trajectoire_tag.is_significatif():
# L'interclassement
interclass = self.interclassements_taggues[aggregat]
# Injection des données dans un dictionnaire
donnees[etudid] |= get_dict_synthese_aggregat(
aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome
)
# Fin de l'aggrégat
# Construction du dataFrame
@ -424,7 +406,9 @@ class JuryPE(object):
for aggregat in aggregats:
# Le dictionnaire par défaut des moyennes
donnees[tag] |= get_defaut_dict_synthese_aggregat(aggregat, self.diplome)
donnees[tag] |= get_defaut_dict_synthese_aggregat(
aggregat, self.diplome
)
# La trajectoire de l'étudiant sur l'aggrégat
trajectoire = self.trajectoires.suivi[etudid][aggregat]
@ -432,26 +416,25 @@ class JuryPE(object):
trajectoire_tagguee = self.trajectoires_tagguees[
trajectoire.trajectoire_id
]
else:
trajectoire_tagguee = None
if tag in trajectoire_tagguee.moyennes_tags:
# L'interclassement
interclass = self.interclassements_taggues[aggregat]
# L'interclassement
interclass = self.interclassements_taggues[aggregat]
# Injection des données dans un dictionnaire
donnees[tag] |= get_dict_synthese_aggregat(aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome)
# Injection des données dans un dictionnaire
donnees[tag] |= get_dict_synthese_aggregat(
aggregat, trajectoire_tagguee, interclass, etudid, tag, self.diplome
)
# Fin de l'aggrégat
# Construction du dataFrame
df = pd.DataFrame.from_dict(donnees, orient="index")
# Tri par nom/prénom
df.sort_values(
by=[("", "", "tag")], inplace=True
)
df.sort_values(by=[("", "", "tag")], inplace=True)
return df
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés.
Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire
@ -587,7 +570,7 @@ def get_dict_synthese_aggregat(
interclassement_taggue: AggregatInterclasseTag,
etudid: int,
tag: str,
diplome: int
diplome: int,
):
"""Renvoie le dictionnaire (à intégrer au tableur excel de synthese)
traduisant les résultats (moy/class) d'un étudiant à une trajectoire tagguée associée
@ -600,66 +583,43 @@ def get_dict_synthese_aggregat(
note = np.nan
# Les données de la trajectoire tagguée pour le tag considéré
if trajectoire_tagguee and tag in trajectoire_tagguee.moyennes_tags:
bilan = trajectoire_tagguee.moyennes_tags[tag]
moy_tag = trajectoire_tagguee.moyennes_tags[tag]
# La moyenne de l'étudiant
note = TableTag.get_note_for_df(bilan, etudid)
# Les données de l'étudiant
note = moy_tag.get_note_for_df(etudid)
# Statistiques sur le groupe
if not pd.isna(note) and note != np.nan:
# Les moyennes de cette trajectoire
donnees |= {
classement = moy_tag.get_class_for_df(etudid)
nmin = moy_tag.get_min_for_df()
nmax = moy_tag.get_max_for_df()
nmoy = moy_tag.get_moy_for_df()
# Statistiques sur le groupe
if not pd.isna(note) and note != np.nan:
# Les moyennes de cette trajectoire
donnees |= {
(descr, "", "note"): note,
(
descr,
NOM_STAT_GROUPE,
"class.",
): TableTag.get_class_for_df(bilan, etudid),
(
descr,
NOM_STAT_GROUPE,
"min",
): TableTag.get_min_for_df(bilan),
(
descr,
NOM_STAT_GROUPE,
"moy",
): TableTag.get_moy_for_df(bilan),
(
descr,
NOM_STAT_GROUPE,
"max",
): TableTag.get_max_for_df(bilan),
(descr, NOM_STAT_GROUPE, "class."): classement,
(descr, NOM_STAT_GROUPE, "min"): nmin,
(descr, NOM_STAT_GROUPE, "moy"): nmoy,
(descr, NOM_STAT_GROUPE, "max"): nmax,
}
# L'interclassement
if tag in interclassement_taggue.moyennes_tags:
bilan = interclassement_taggue.moyennes_tags[tag]
moy_tag = interclassement_taggue.moyennes_tags[tag]
if not pd.isna(note) and note != np.nan:
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
classement = moy_tag.get_class_for_df(etudid)
nmin = moy_tag.get_min_for_df()
nmax = moy_tag.get_max_for_df()
nmoy = moy_tag.get_moy_for_df()
if not pd.isna(note) and note != np.nan:
nom_stat_promo = f"{NOM_STAT_PROMO} {diplome}"
donnees |= {
(descr, nom_stat_promo, "class."): classement,
(descr, nom_stat_promo, "min"): nmin,
(descr, nom_stat_promo, "moy"): nmoy,
(descr, nom_stat_promo, "max"): nmax,
}
donnees |= {
(
descr,
nom_stat_promo,
"class.",
): TableTag.get_class_for_df(bilan, etudid),
(
descr,
nom_stat_promo,
"min",
): TableTag.get_min_for_df(bilan),
(
descr,
nom_stat_promo,
"moy",
): TableTag.get_moy_for_df(bilan),
(
descr,
nom_stat_promo,
"max",
): TableTag.get_max_for_df(bilan),
}
return donnees

View File

@ -38,7 +38,7 @@ Created on Fri Sep 9 09:15:05 2016
import numpy as np
import app.pe.pe_etudiant
from app import db, log
from app import db, log, ScoValueError
from app.comp import res_sem, moy_ue, moy_sem
from app.comp.moy_sem import comp_ranks_series
from app.comp.res_compat import NotesTableCompat
@ -49,7 +49,7 @@ from app.models.moduleimpls import ModuleImpl
from app.scodoc import sco_tag_module
from app.scodoc.codes_cursus import UE_SPORT
import app.pe.pe_affichage as pe_affichage
from app.pe.pe_tabletags import TableTag, TAGS_RESERVES
from app.pe.pe_tabletags import TableTag, TAGS_RESERVES, MoyenneTag
import pandas as pd
@ -94,44 +94,51 @@ class SemestreTag(TableTag):
# Les tags :
## Saisis par l'utilisateur
self.tags_personnalises = get_synthese_tags_personnalises_semestre(
tags_personnalises = get_synthese_tags_personnalises_semestre(
self.nt.formsemestre
)
## Déduit des compétences
self.tags_competences = get_noms_competences_from_ues(self.nt.formsemestre)
dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
# Supprime les doublons dans les tags
tags_reserves = TAGS_RESERVES + list(self.tags_competences.values())
for tag in self.tags_personnalises:
if tag in tags_reserves:
del self.tags_personnalises[tag]
pe_affichage.pe_print(f"Supprime le tag {tag}")
self.tags = (
list(tags_personnalises.keys())
+ list(dict_ues_competences.values())
+ ["but"]
)
"""Tags du semestre taggué"""
## Vérifie l'unicité des tags
if len(set(self.tags)) != len(self.tags):
raise ScoValueError(
f"""Erreur dans le module PE : L'un des tags saisis dans le programme
fait parti des tags réservés (par ex. "comp. <titre_compétence>"). Modifiez les
tags de votre programme"""
)
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
self.moyennes_tags = {}
for tag in self.tags_personnalises:
for tag in tags_personnalises:
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
moy_gen_tag = self.compute_moyenne_tag(tag)
self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag)
moy_gen_tag = self.compute_moyenne_tag(tag, tags_personnalises)
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
# Ajoute les moyennes générales de BUT pour le semestre considéré
moy_gen_but = self.nt.etud_moy_gen
moy_gen_but = pd.to_numeric(moy_gen_but, errors="coerce")
self.moyennes_tags["but"] = self.comp_moy_et_stat(moy_gen_but)
self.moyennes_tags["but"] = MoyenneTag("but", moy_gen_but)
# Ajoute les moyennes par compétence
for ue_id, competence in self.tags_competences.items():
for ue_id, competence in dict_ues_competences.items():
moy_ue = self.nt.etud_moy_ue[ue_id]
self.moyennes_tags[competence] = self.comp_moy_et_stat(moy_ue)
self.moyennes_tags[competence] = MoyenneTag(competence, moy_ue)
self.tags_sorted = self.get_all_tags()
"""Tags (personnalisés+compétences) par ordre alphabétique"""
# Synthétise l'ensemble des moyennes dans un dataframe
self.tags_sorted = sorted(
self.moyennes_tags
) # les tags (personnalisés+compétences) par ordre alphabétique
self.notes = (
self.df_notes()
) # Le dataframe synthétique des notes (=moyennes par tag)
self.notes = self.df_notes()
"""Dataframe synthétique des notes par tag"""
pe_affichage.pe_print(
f" => Traitement des tags {', '.join(self.tags_sorted)}"
@ -141,9 +148,10 @@ class SemestreTag(TableTag):
"""Nom affiché pour le semestre taggué"""
return app.pe.pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
def compute_moyenne_tag(self, tag: str) -> list:
def compute_moyenne_tag(self, tag: str, tags_infos: dict) -> pd.Series:
"""Calcule la moyenne des étudiants pour le tag indiqué,
pour ce SemestreTag.
pour ce SemestreTag, en ayant connaissance des informations sur
les tags (dictionnaire donnant les coeff de repondération)
Sont pris en compte les modules implémentés associés au tag,
avec leur éventuel coefficient de **repondération**, en utilisant les notes
@ -151,8 +159,8 @@ class SemestreTag(TableTag):
Force ou non le calcul de la moyenne lorsque des notes sont manquantes.
Renvoie les informations sous la forme d'une liste
[ (moy, somme_coeff_normalise, etudid), ...]
Returns:
La série des moyennes
"""
"""Adaptation du mask de calcul des moyennes au tag visé"""
@ -163,13 +171,13 @@ class SemestreTag(TableTag):
"""Désactive tous les modules qui ne sont pas pris en compte pour ce tag"""
for i, modimpl in enumerate(self.formsemestre.modimpls_sorted):
if modimpl.moduleimpl_id not in self.tags_personnalises[tag]:
if modimpl.moduleimpl_id not in tags_infos[tag]:
modimpls_mask[i] = False
"""Applique la pondération des coefficients"""
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
for modimpl_id in self.tags_personnalises[tag]:
ponderation = self.tags_personnalises[tag][modimpl_id]["ponderation"]
for modimpl_id in tags_infos[tag]:
ponderation = tags_infos[tag][modimpl_id]["ponderation"]
modimpl_coefs_ponderes_df[modimpl_id] *= ponderation
"""Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)"""

View File

@ -40,6 +40,7 @@ Created on Thu Sep 8 09:36:33 2016
import datetime
import numpy as np
from app import ScoValueError
from app.comp.moy_sem import comp_ranks_series
from app.pe import pe_affichage
from app.scodoc import sco_utils as scu
@ -48,63 +49,35 @@ import pandas as pd
TAGS_RESERVES = ["but"]
class MoyenneTag():
class TableTag(object):
def __init__(self):
"""Classe centralisant différentes méthodes communes aux
SemestreTag, TrajectoireTag, AggregatInterclassTag
def __init__(self, tag: str, notes: pd.Series):
"""Classe centralisant la synthèse des moyennes/classements d'une série
d'étudiants à un tag donné, en stockant un dictionnaire :
``
{
"notes": la Serie pandas des notes (float),
"classements": la Serie pandas des classements (float),
"min": la note minimum,
"max": la note maximum,
"moy": la moyenne,
"nb_inscrits": le nombre d'étudiants ayant une note,
}
``
Args:
tag: Un tag
note: Une série de notes (moyenne) sous forme d'un pd.Series()
"""
pass
self.tag = tag
"""Le tag associé à la moyenne"""
self.synthese = self.comp_moy_et_stat(notes)
"""La synthèse des notes/classements/statistiques"""
# -----------------------------------------------------------------------------------------------------------
def get_all_tags(self):
"""Liste des tags de la table, triée par ordre alphabétique
Returns:
Liste de tags triés par ordre alphabétique
"""
return sorted(self.moyennes_tags.keys())
def df_moyennes_et_classements(self):
"""Renvoie un dataframe listant toutes les moyennes,
et les classements des étudiants pour tous les tags
"""
etudiants = self.etudiants
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
for tag in self.get_all_tags():
df = df.join(self.moyennes_tags[tag]["notes"].rename(f"Moy {tag}"))
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"Class {tag}"))
return df
def df_notes(self):
"""Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags
Returns:
Un dataframe etudids x tag (avec tag par ordre alphabétique)
"""
tags = self.get_all_tags()
if tags:
dict_series = {tag: self.moyennes_tags[tag]["notes"] for tag in tags}
df = pd.DataFrame(dict_series)
return df
else:
return None
def str_tagtable(self):
"""Renvoie une chaine de caractère listant toutes les moyennes,
les rangs des étudiants pour tous les tags."""
etudiants = self.etudiants
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
for tag in self.get_all_tags():
df = df.join(self.moyennes_tags[tag]["notes"].rename(f"moy {tag}"))
df = df.join(self.moyennes_tags[tag]["classements"].rename(f"class {tag}"))
return df.to_csv(sep=";")
def __eq__(self, other):
"""Egalité de deux MoyenneTag lorsque leur tag sont identiques"""
return self.tag == other.tag
def comp_moy_et_stat(self, notes: pd.Series) -> dict:
"""Calcule et structure les données nécessaires au PE pour une série
@ -131,7 +104,7 @@ class TableTag(object):
(_, class_gen_ue_non_nul) = comp_ranks_series(notes_non_nulles)
# Les classements (toutes notes confondues, avec NaN si pas de notes)
class_gen_ue = pd.Series(np.nan, index=notes.index, dtype="Int64")
class_gen_ue = pd.Series(np.nan, index=notes.index) #, dtype="Int64")
class_gen_ue[indices] = class_gen_ue_non_nul[indices]
synthese = {
@ -140,42 +113,97 @@ class TableTag(object):
"min": notes.min(),
"max": notes.max(),
"moy": notes.mean(),
"nb_inscrits": len(indices),
"nb_inscrits": sum(indices),
}
return synthese
@classmethod
def get_min_for_df(cls, bilan: dict) -> float:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
revoie le min renseigné pour affichage dans un df"""
return round(bilan["min"], 2)
def get_note_for_df(self, etudid: int):
"""Note d'un étudiant donné par son etudid"""
return round(self.synthese["notes"].loc[etudid], 2)
@classmethod
def get_max_for_df(cls, bilan: dict) -> float:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie le max renseigné pour affichage dans un df"""
return round(bilan["max"], 2)
def get_min_for_df(self) -> float:
"""Min renseigné pour affichage dans un df"""
return round(self.synthese["min"], 2)
@classmethod
def get_moy_for_df(cls, bilan: dict) -> float:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie la moyenne renseignée pour affichage dans un df"""
return round(bilan["moy"], 2)
def get_max_for_df(self) -> float:
"""Max renseigné pour affichage dans un df"""
return round(self.synthese["max"], 2)
@classmethod
def get_class_for_df(cls, bilan: dict, etudid: int) -> str:
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie le classement ramené au nombre d'inscrits,
def get_moy_for_df(self) -> float:
"""Moyenne renseignée pour affichage dans un df"""
return round(self.synthese["moy"], 2)
def get_class_for_df(self, etudid: int) -> str:
"""Classement ramené au nombre d'inscrits,
pour un étudiant donné par son etudid"""
classement = bilan['classements'].loc[etudid]
classement = self.synthese['classements'].loc[etudid]
nb_inscrit = self.synthese['nb_inscrits']
if not pd.isna(classement):
return f"{classement}/{bilan['nb_inscrits']}"
classement = int(classement)
return f"{classement}/{nb_inscrit}"
else:
return pe_affichage.SANS_NOTE
@classmethod
def get_note_for_df(cls, bilan: dict, etudid: int):
"""Partant d'un dictionnaire `bilan` généralement une moyennes_tags pour un tag donné,
renvoie la note (moyenne)
pour un étudiant donné par son etudid"""
return round(bilan["notes"].loc[etudid], 2)
def is_significatif(self) -> bool:
"""Indique si la moyenne est significative (c'est-à-dire à des notes)"""
return self.synthese["nb_inscrits"] > 0
class TableTag(object):
def __init__(self):
"""Classe centralisant différentes méthodes communes aux
SemestreTag, TrajectoireTag, AggregatInterclassTag
"""
pass
# -----------------------------------------------------------------------------------------------------------
def get_all_tags(self):
"""Liste des tags de la table, triée par ordre alphabétique,
extraite des clés du dictionnaire ``moyennes_tags`` connues (tags en doublon
possible).
Returns:
Liste de tags triés par ordre alphabétique
"""
return sorted(list(self.moyennes_tags.keys()))
def df_moyennes_et_classements(self) -> pd.DataFrame:
"""Renvoie un dataframe listant toutes les moyennes,
et les classements des étudiants pour tous les tags.
Est utilisé pour afficher le détail d'un tableau taggué
(semestres, trajectoires ou aggrégat)
Returns:
Le dataframe des notes et des classements
"""
etudiants = self.etudiants
df = pd.DataFrame.from_dict(etudiants, orient="index", columns=["nom"])
tags_tries = self.get_all_tags()
for tag in tags_tries:
moy_tag = self.moyennes_tags[tag]
df = df.join(moy_tag.synthese["notes"].rename(f"Moy {tag}"))
df = df.join(moy_tag.synthese["classements"].rename(f"Class {tag}"))
return df
def df_notes(self) -> pd.DataFrame | None:
"""Renvoie un dataframe (etudid x tag) listant toutes les moyennes par tags
Returns:
Un dataframe etudids x tag (avec tag par ordre alphabétique)
"""
tags_tries = self.get_all_tags()
if tags_tries:
dict_series = {}
for tag in tags_tries:
# Les moyennes associés au tag
moy_tag = self.moyennes_tags[tag]
dict_series[tag] = moy_tag.synthese["notes"]
df = pd.DataFrame(dict_series)
return df

View File

@ -6,43 +6,44 @@ from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date
class Trajectoire:
"""Modélise, pour un aggrégat visé (par ex: 'S2', '3S', '2A')
et un ensemble d'étudiants donnés,
la combinaison des formsemestres des étudiants amenant à un semestre
terminal visé.
Si l'aggrégat est un semestre de type Si, elle stocke le (ou les)
formsemestres de numéro i qu'ont suivis l'étudiant pour atteindre le Si
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
Pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
terminal de la trajectoire (par ex: ici un S3).
Ces semestres peuvent être :
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
* des S1+S2+(année de césure)+S3 si césure, ...
"""
def __init__(self, nom_aggregat: str, semestre_final: FormSemestre):
"""Modélise un ensemble de formsemestres d'étudiants
amenant à un semestre terminal
amenant à un semestre terminal, au sens d'un aggrégat (par ex: 'S2', '3S', '2A').
Si l'aggrégat est un semestre de type Si, elle stocke le (ou les)
formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
Pour des aggrégats de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
terminal de la trajectoire (par ex: ici un S3).
Ces semestres peuvent être :
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
* des S1+S2+(année de césure)+S3 si césure, ...
Args:
nom_aggregat: Un nom d'aggrégat (par ex: '5S')
semestre_final: Le semestre final de l'aggrégat
"""
self.nom = nom_aggregat
self.semestre_final = semestre_final
self.trajectoire_id = (nom_aggregat, semestre_final.formsemestre_id)
"""Nom de l'aggrégat"""
self.formsemestre_final = semestre_final
"""FormSemestre terminal de la trajectoire"""
self.trajectoire_id = (nom_aggregat, semestre_final.formsemestre_id)
"""Identifiant de la trajectoire"""
"""Les semestres à aggréger"""
self.semestres_aggreges = {}
"""Semestres aggrégés"""
def add_semestres_a_aggreger(self, semestres: dict[int:FormSemestre]):
"""Ajoute des semestres au semestre à aggréger
"""Ajout de semestres aux semestres à aggréger
Args:
semestres: Dictionnaire ``{fid: FormSemestre(fid)} à ajouter``
semestres: Dictionnaire ``{fid: FormSemestre(fid)}`` à ajouter
"""
self.semestres_aggreges = self.semestres_aggreges | semestres
@ -55,27 +56,30 @@ class Trajectoire:
semestre = self.semestres_aggreges[fid]
noms.append(f"S{semestre.semestre_id}({fid})")
noms = sorted(noms)
repr = f"{self.nom} ({self.semestre_final.formsemestre_id}) {self.semestre_final.date_fin.year}"
repr = f"{self.nom} ({self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}"
if verbose and noms:
repr += " - " + "+".join(noms)
return repr
class TrajectoiresJuryPE:
"""Centralise toutes les trajectoires du jury PE"""
def __init__(self, annee_diplome: int):
"""
"""Classe centralisant toutes les trajectoires des étudiants à prendre
en compte dans un jury PE
Args:
annee_diplome: L'année de diplomation
"""
self.annee_diplome = annee_diplome
"""Toutes les trajectoires possibles"""
self.trajectoires: dict[tuple:Trajectoire] = {}
"""Quelle trajectoires pour quel étudiant :
dictionnaire {etudid: {nom_aggregat: Trajectoire}}"""
"""Ensemble des trajectoires recensées : {(aggregat, fid_terminal): Trajectoire}"""
self.suivi: dict[int:str] = {}
"""Dictionnaire associant, pour chaque étudiant et pour chaque aggrégat,
sa trajectoire : {etudid: {nom_aggregat: Trajectoire}}"""
def cree_trajectoires(self, etudiants: EtudiantsJuryPE):
"""Créé toutes les trajectoires, au regard du cursus des étudiants
@ -122,9 +126,6 @@ class TrajectoiresJuryPE:
"""Mémoire la trajectoire suivie par l'étudiant"""
self.suivi[etudid][nom_aggregat] = trajectoire
"""Vérifications"""
# dernier_semestre_aggregat = get_dernier_semestre_en_date(semestres_aggreges)
# assert dernier_semestre_aggregat == formsemestre_terminal
def get_trajectoires_etudid(trajectoires, etudid):
@ -142,26 +143,3 @@ def get_trajectoires_etudid(trajectoires, etudid):
return liste
def get_semestres_a_aggreger(self, aggregat: str, formsemestre_id_terminal: int):
"""Pour un nom d'aggrégat donné (par ex: 'S3') et un semestre terminal cible
identifié par son formsemestre_id (par ex: 'S3 2022-2023'),
renvoie l'ensemble des semestres à prendre en compte dans
l'aggrégat sous la forme d'un dictionnaire {fid: FormSemestre(fid)}.
Fusionne les cursus individuels des étudiants, dont le cursus correspond
à l'aggrégat visé.
Args:
aggregat: Un aggrégat (par ex. 1A, 2A, 3S, 6S)
formsemestre_id_terminal: L'identifiant du formsemestre terminal de l'aggrégat, devant correspondre au
dernier semestre de l'aggrégat
"""
noms_semestres_aggreges = pe_comp.PARCOURS[aggregat]["aggregat"]
formsemestres = {}
for etudid in self.cursus:
cursus_etudiant = self.cursus[etudid][aggregat]
if formsemestre_id_terminal in cursus_etudiant:
formsemestres_etudiant = cursus_etudiant[formsemestre_id_terminal]
formsemestres = formsemestres | formsemestres_etudiant
return formsemestres

View File

@ -43,7 +43,7 @@ import pandas as pd
import numpy as np
from app.pe.pe_trajectoire import Trajectoire
from app.pe.pe_tabletags import TableTag
from app.pe.pe_tabletags import TableTag, MoyenneTag
class TrajectoireTag(TableTag):
@ -58,6 +58,9 @@ class TrajectoireTag(TableTag):
Par ex: fusion d'un parcours ['S1', 'S2', 'S3'] donnant un nom_combinaison = '3S'
Args:
trajectoire: Une trajectoire (aggrégat+semestre terminal)
semestres_taggues: Les données sur les semestres taggués
"""
TableTag.__init__(self)
@ -68,8 +71,9 @@ class TrajectoireTag(TableTag):
# Le nom de la trajectoire tagguée (identique à la trajectoire)
self.nom = self.get_repr()
self.formsemestre_terminal = trajectoire.semestre_final
self.formsemestre_terminal = trajectoire.formsemestre_final
"""Le formsemestre terminal"""
# Les résultats du formsemestre terminal
nt = load_formsemestre_results(self.formsemestre_terminal)
@ -100,11 +104,11 @@ class TrajectoireTag(TableTag):
self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted)
"""Calcul les moyennes par tag sous forme d'un dataframe"""
self.moyennes_tags = {}
self.moyennes_tags: dict[str, MoyenneTag] = {}
"""Synthétise les moyennes/classements par tag (qu'ils soient personnalisé ou de compétences)"""
for tag in self.tags_sorted:
moy_gen_tag = self.notes[tag]
self.moyennes_tags[tag] = self.comp_moy_et_stat(moy_gen_tag)
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
def get_repr(self, verbose=False) -> str:
"""Renvoie une représentation textuelle (celle de la trajectoire sur laquelle elle

View File

@ -39,6 +39,7 @@ from app import db, email
from app import log
from app.models import Admission, Identite
from app.models.etudiants import (
check_etud_duplicate_code,
input_civilite,
input_civilite_etat_civil,
make_etud_args,
@ -229,74 +230,12 @@ def check_nom_prenom_homonyms(
return True, query.all()
def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True):
"""Vérifie que le code n'est pas dupliqué.
Raises ScoGenError si problème.
"""
etudid = args.get("etudid", None)
if args.get(code_name, None):
etuds = identite_list(cnx, {code_name: str(args[code_name])})
duplicate = False
if edit:
duplicate = (len(etuds) > 1) or (
(len(etuds) == 1) and etuds[0]["id"] != args["etudid"]
)
else:
duplicate = len(etuds) > 0
if duplicate:
listh = [] # liste des doubles
for e in etuds:
listh.append(
f"""Autre étudiant: <a href="{
url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=e["etudid"]
)}">{e['nom']} {e['prenom']}</a>"""
)
if etudid:
OK = "retour à la fiche étudiant"
dest_endpoint = "scolar.fiche_etud"
parameters = {"etudid": etudid}
else:
if "tf_submitted" in args:
del args["tf_submitted"]
OK = "Continuer"
dest_endpoint = "scolar.etudident_create_form"
parameters = args
else:
OK = "Annuler"
dest_endpoint = "notes.index_html"
parameters = {}
if not disable_notify:
err_page = f"""<h3><h3>Code étudiant ({code_name}) dupliqué !</h3>
<p class="help">Le {code_name} {args[code_name]} est déjà utilisé: un seul étudiant peut avoir
ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
</p>
<ul><li>
{ '</li><li>'.join(listh) }
</li></ul>
<p>
<a href="{ url_for(dest_endpoint, scodoc_dept=g.scodoc_dept, **parameters) }
">{OK}</a>
</p>
"""
else:
err_page = f"""<h3>Code étudiant ({code_name}) dupliqué !</h3>"""
log(f"*** error: code {code_name} duplique: {args[code_name]}")
raise ScoGenError(err_page)
def identite_edit(cnx, args, disable_notify=False):
"""Modifie l'identite d'un étudiant.
Si pref notification et difference, envoie message notification, sauf si disable_notify
"""
_check_duplicate_code(
cnx, args, "code_nip", disable_notify=disable_notify, edit=True
)
_check_duplicate_code(
cnx, args, "code_ine", disable_notify=disable_notify, edit=True
)
check_etud_duplicate_code(args, "code_nip", edit=True)
check_etud_duplicate_code(args, "code_ine", edit=True)
notify_to = None
if not disable_notify:
try:
@ -325,16 +264,14 @@ def identite_edit(cnx, args, disable_notify=False):
def identite_create(cnx, args):
"check unique etudid, then create"
_check_duplicate_code(cnx, args, "code_nip", edit=False)
_check_duplicate_code(cnx, args, "code_ine", edit=False)
check_etud_duplicate_code(args, "code_nip", edit=False)
check_etud_duplicate_code(args, "code_ine", edit=False)
if "etudid" in args:
etudid = args["etudid"]
r = identite_list(cnx, {"etudid": etudid})
if r:
raise ScoValueError(
"Code identifiant (etudid) déjà utilisé ! (%s)" % etudid
)
raise ScoValueError(f"Code identifiant (etudid) déjà utilisé ! ({etudid})")
return _identiteEditor.create(cnx, args)

View File

@ -40,7 +40,7 @@ from app import db
from app.auth.models import User
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Evaluation, FormSemestre
from app.models import Evaluation, FormSemestre, ModuleImpl
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
@ -280,82 +280,14 @@ def do_evaluation_etat(
}
def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
"""Liste les évaluations de tous les modules de ce semestre.
Triée par module, numero desc, date_debut desc
Donne pour chaque eval son état (voir do_evaluation_etat)
{ evaluation_id,nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif ... }
Exemple:
[ {
'coefficient': 1.0,
'description': 'QCM et cas pratiques',
'etat': {
'evalattente': False,
'evalcomplete': True,
'evaluation_id': 'GEAEVAL82883',
'gr_incomplets': [],
'gr_moyennes': [{
'gr_median': '12.00', # sur 20
'gr_moy': '11.88',
'gr_nb_att': 0,
'gr_nb_notes': 166,
'group_id': 'GEAG266762',
'group_name': None
}],
'groups': {'GEAG266762': {'etudid': 'GEAEID80603',
'group_id': 'GEAG266762',
'group_name': None,
'partition_id': 'GEAP266761'}
},
'last_modif': datetime.datetime(2015, 12, 3, 15, 15, 16),
'median': '12.00',
'moy': '11.84',
'nb_abs': 2,
'nb_att': 0,
'nb_inscrits': 166,
'nb_neutre': 0,
'nb_notes': 168,
'nb_notes_total': 169
},
'evaluation_id': 'GEAEVAL82883',
'evaluation_type': 0,
'heure_debut': datetime.time(8, 0),
'heure_fin': datetime.time(9, 30),
'jour': datetime.date(2015, 11, 3), // vide => 1/1/1900
'moduleimpl_id': 'GEAMIP80490',
'note_max': 20.0,
'numero': 0,
'publish_incomplete': 0,
'visibulletin': 1} ]
"""
req = """SELECT E.id AS evaluation_id, E.*
FROM notes_evaluation E, notes_moduleimpl MI
WHERE MI.formsemestre_id = %(formsemestre_id)s
and MI.id = E.moduleimpl_id
ORDER BY MI.id, numero desc, date_debut desc
"""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(req, {"formsemestre_id": formsemestre_id})
res = cursor.dictfetchall()
# etat de chaque evaluation:
for r in res:
if with_etat:
r["etat"] = do_evaluation_etat(r["evaluation_id"])
r["jour"] = r["date_debut"] or datetime.date(1900, 1, 1)
return res
def _eval_etat(evals):
"""evals: list of mappings (etats)
def _summarize_evals_etats(evals: list[dict]) -> dict:
"""Synthétise les états d'une liste d'évaluations
evals: list of mappings (etats),
utilise e["etat"]["evalcomplete"], e["etat"]["nb_notes"], e["etat"]["last_modif"]
-> nb_eval_completes, nb_evals_en_cours,
nb_evals_vides, date derniere modif
Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
"""
nb_evals_completes, nb_evals_en_cours, nb_evals_vides = 0, 0, 0
dates = []
@ -370,11 +302,8 @@ def _eval_etat(evals):
if last_modif is not None:
dates.append(e["etat"]["last_modif"])
if dates:
dates = scu.sort_dates(dates)
last_modif = dates[-1] # date de derniere modif d'une note dans un module
else:
last_modif = ""
# date de derniere modif d'une note dans un module
last_modif = sorted(dates)[-1] if dates else ""
return {
"nb_evals_completes": nb_evals_completes,
@ -384,37 +313,42 @@ def _eval_etat(evals):
}
def do_evaluation_etat_in_sem(formsemestre_id):
"""-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
date derniere modif, attente
XXX utilisé par
- formsemestre_status_head
- gen_formsemestre_recapcomplet_xml
- gen_formsemestre_recapcomplet_json
"nb_evals_completes"
"nb_evals_en_cours"
"nb_evals_vides"
"date_derniere_note"
"last_modif"
"attente"
def do_evaluation_etat_in_sem(formsemestre: FormSemestre) -> dict:
"""-> { nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
date derniere modif, attente }
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Note: utilisé par
# - formsemestre_status_head
# nb_evals_completes, nb_evals_en_cours, nb_evals_vides, last_modif
# pour la ligne
# Évaluations: 20 ok, 8 en cours, 5 vides (dernière note saisie le 11/01/2024 à 19h49)
# attente
#
# - gen_formsemestre_recapcomplet_xml
# - gen_formsemestre_recapcomplet_json
# nb_evals_completes, nb_evals_en_cours, nb_evals_vides, last_modif
#
# "nb_evals_completes"
# "nb_evals_en_cours"
# "nb_evals_vides"
# "last_modif"
# "attente"
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
evals = nt.get_evaluations_etats()
etat = _eval_etat(evals)
evaluations_etats = nt.get_evaluations_etats()
# raccordement moche...
etat = _summarize_evals_etats([{"etat": v} for v in evaluations_etats.values()])
# Ajoute information sur notes en attente
etat["attente"] = len(nt.get_moduleimpls_attente()) > 0
return etat
def do_evaluation_etat_in_mod(nt, moduleimpl_id):
def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
"""état des évaluations dans ce module"""
evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
etat = _eval_etat(evals)
evals = nt.get_mod_evaluation_etat_list(modimpl)
etat = _summarize_evals_etats(evals)
# Il y a-t-il des notes en attente dans ce module ?
etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente
etat["attente"] = nt.modimpls_results[modimpl.id].en_attente
return etat

View File

@ -230,3 +230,15 @@ class APIInvalidParams(Exception):
class ScoFormationConflict(Exception):
"""Conflit cohérence formation (APC)"""
class ScoTemporaryError(ScoValueError):
"""Erreurs temporaires rarissimes (caches ?)"""
def __init__(self, msg: str = ""):
msg = """
<p>"Erreur temporaire</p>
<p>Veuillez -essayer. Si le problème persiste, merci de contacter l'assistance ScoDoc
</p>
"""
super().__init__(msg)

View File

@ -627,9 +627,7 @@ def formsemestre_description_table(
# car l'UE de rattachement n'a pas d'intérêt en BUT
rows.append(ue_info)
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
moduleimpl_id=modimpl.id
)
mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module
enseignants = ", ".join(ens.get_prenomnom() for ens in modimpl.enseignants)
row = {
@ -638,7 +636,7 @@ def formsemestre_description_table(
"Code": modimpl.module.code or "",
"Module": modimpl.module.abbrev or modimpl.module.titre,
"_Module_class": "scotext",
"Inscrits": len(mod_inscrits),
"Inscrits": mod_nb_inscrits,
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],
"_Responsable_class": "scotext",
"Enseignants": enseignants,
@ -680,7 +678,7 @@ def formsemestre_description_table(
if with_evals:
# Ajoute lignes pour evaluations
evals = nt.get_mod_evaluation_etat_list(modimpl.id)
evals = nt.get_mod_evaluation_etat_list(modimpl)
evals.reverse() # ordre chronologique
# Ajoute etat:
eval_rows = []
@ -942,10 +940,10 @@ def html_expr_diagnostic(diagnostics):
def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None):
"""En-tête HTML des pages "semestre" """
sem: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
if not sem:
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
if not formsemestre:
raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)")
formation: Formation = sem.formation
formation: Formation = formsemestre.formation
parcours = formation.get_cursus()
page_title = page_title or "Modules de "
@ -957,25 +955,25 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
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)}"
scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id)}"
class="discretelink" title="Formation {
formation.acronyme}, v{formation.version}">{formation.titre}</a>
""",
]
if sem.semestre_id >= 0:
H.append(f", {parcours.SESSION_NAME} {sem.semestre_id}")
if sem.modalite:
H.append(f"&nbsp;en {sem.modalite}")
if sem.etapes:
if formsemestre.semestre_id >= 0:
H.append(f", {parcours.SESSION_NAME} {formsemestre.semestre_id}")
if formsemestre.modalite:
H.append(f"&nbsp;en {formsemestre.modalite}")
if formsemestre.etapes:
H.append(
f"""&nbsp;&nbsp;&nbsp;(étape <b><tt>{
sem.etapes_apo_str() or "-"
formsemestre.etapes_apo_str() or "-"
}</tt></b>)"""
)
H.append("</td></tr>")
if formation.is_apc():
# Affiche les parcours BUT cochés. Si aucun, tous ceux du référentiel.
sem_parcours = sem.get_parcours_apc()
sem_parcours = formsemestre.get_parcours_apc()
H.append(
f"""
<tr><td class="fichetitre2">Parcours: </td>
@ -984,7 +982,7 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
"""
)
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id)
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
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
@ -1002,11 +1000,11 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
"""<span class="fontred">Il y a des notes en attente !</span>
Le classement des étudiants n'a qu'une valeur indicative."""
)
if sem.bul_hide_xml:
if formsemestre.bul_hide_xml:
warnings.append("""Bulletins non publiés sur la passerelle.""")
if sem.block_moyennes:
if formsemestre.block_moyennes:
warnings.append("Calcul des moyennes bloqué !")
if sem.semestre_id >= 0 and not sem.est_sur_une_annee():
if formsemestre.semestre_id >= 0 and not formsemestre.est_sur_une_annee():
warnings.append("""<em>Ce semestre couvre plusieurs années scolaires !</em>""")
if warnings:
H += [
@ -1028,18 +1026,14 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
# S'assure que les groupes de parcours sont à jour:
if int(check_parcours):
formsemestre.setup_parcours_groups()
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
)
modimpls = formsemestre.modimpls_sorted
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"]]
)
mails_enseignants.add(sco_users.user_info(modimpl.responsable_id)["email"])
mails_enseignants |= {u.email for u in modimpl.enseignants if u.email}
can_edit = formsemestre.can_be_edited_by(current_user)
can_change_all_notes = current_user.has_permission(Permission.EditAllNotes) or (
@ -1089,13 +1083,13 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
if nt.parcours.APC_SAE:
# BUT: tableau ressources puis SAE
ressources = [
m for m in modimpls if m["module"]["module_type"] == ModuleType.RESSOURCE
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]
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)
if m.module.module_type not in (ModuleType.RESSOURCE, ModuleType.SAE)
]
H += [
f"""
@ -1136,7 +1130,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
modimpls_classic = [
m
for m in modimpls
if m["module"]["module_type"] not in (ModuleType.RESSOURCE, ModuleType.SAE)
if m.module.module_type not in (ModuleType.RESSOURCE, ModuleType.SAE)
]
H += [
"<p>",
@ -1168,8 +1162,10 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
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))
f"""<p>
<a class="stdlink" href="mailto:?cc={','.join(adrlist)}">Courrier aux {
len(adrlist)} enseignants du semestre</a>
</p>"""
)
return "".join(H) + html_sco_header.sco_footer()
@ -1189,7 +1185,7 @@ _TABLEAU_MODULES_FOOT = """</table>"""
def formsemestre_tableau_modules(
modimpls: list[dict],
modimpls: list[ModuleImpl],
nt,
formsemestre: FormSemestre,
can_edit=True,
@ -1200,11 +1196,11 @@ def formsemestre_tableau_modules(
H = []
prev_ue_id = None
for modimpl in modimpls:
mod: Module = db.session.get(Module, modimpl["module_id"])
mod: Module = modimpl.module
moduleimpl_status_url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl["moduleimpl_id"],
moduleimpl_id=modimpl.id,
)
mod_descr = "Module " + (mod.titre or "")
if mod.is_apc():
@ -1221,48 +1217,45 @@ def formsemestre_tableau_modules(
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 = sco_users.user_info(modimpl.responsable_id)["nomcomplet"]
if modimpl.enseignants.count():
mod_ens += " (resp.), " + ", ".join(
[sco_users.user_info(e["ens_id"])["nomcomplet"] for e in modimpl["ens"]]
[u.get_nomcomplet() for u in modimpl.enseignants]
)
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"]
mod_nb_inscrits = nt.modimpls_results[modimpl.id].nb_inscrits_module
ue = modimpl.module.ue
if show_ues and (prev_ue_id != ue.id):
prev_ue_id = ue.id
titre = ue.titre
if use_ue_coefs:
titre += f""" <b>(coef. {ue["coefficient"] or 0.0})</b>"""
titre += f""" <b>(coef. {ue.coefficient or 0.0})</b>"""
H.append(
f"""<tr class="formsemestre_status_ue"><td colspan="4">
<span class="status_ue_acro">{ue["acronyme"]}</span>
<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
formsemestre.id, 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"] )
url_for("notes.delete_ue_expr", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ue_id=ue.id )
}
">supprimer</a></span>"""
)
H.append("</td></tr>")
if modimpl["ue"]["type"] != codes_cursus.UE_STANDARD:
if ue.type != codes_cursus.UE_STANDARD:
fontorange = " fontorange" # style css additionnel
else:
fontorange = ""
etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl["moduleimpl_id"])
etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl)
# if nt.parcours.APC_SAE:
# tbd style si module non conforme
if (
@ -1282,10 +1275,10 @@ def formsemestre_tableau_modules(
<td class="scotext"><a href="{moduleimpl_status_url}" 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="formsemestre_status_inscrits">{mod_nb_inscrits}</td>
<td class="resp scotext">
<a class="discretelink" href="{moduleimpl_status_url}" title="{mod_ens}">{
sco_users.user_info(modimpl["responsable_id"])["prenomnom"]
sco_users.user_info(modimpl.responsable_id)["prenomnom"]
}</a>
</td>
<td>
@ -1339,10 +1332,7 @@ def formsemestre_tableau_modules(
)
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"])
]
e["etat"]["nb_notes"] for e in nt.get_mod_evaluation_etat_list(modimpl)
)
H.append(
f"""<td class="malus">

View File

@ -508,15 +508,20 @@ def students_import_admission(
H = [html_sco_header.sco_header(page_title="Import données admissions")]
H.append("<p>Import terminé !</p>")
H.append(
'<p><a class="stdlink" href="%s">Continuer</a></p>'
% url_for(
f"""<p><a class="stdlink" href="{ url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
}">Continuer</a></p>"""
)
if diag:
H.append("<p>Diagnostic: <ul><li>%s</li></ul></p>" % "</li><li>".join(diag))
H.append(
f"""<p>Diagnostic: <ul><li>{
"</li><li>".join(diag)
}</li></ul></p>
"""
)
return "\n".join(H) + html_sco_header.sco_footer()

View File

@ -367,7 +367,7 @@ def gen_formsemestre_recapcomplet_xml(
doc = ElementTree.Element(
"recapsemestre", formsemestre_id=str(formsemestre_id), date=docdate
)
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id)
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
doc.append(
ElementTree.Element(
"evals_info",
@ -408,7 +408,7 @@ def gen_formsemestre_recapcomplet_json(
docdate = ""
else:
docdate = datetime.datetime.now().isoformat()
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id)
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
js_data = {
"docdate": docdate,
"formsemestre_id": formsemestre_id,

View File

@ -1440,17 +1440,6 @@ EMO_PREV_ARROW = "&#10094;"
EMO_NEXT_ARROW = "&#10095;"
def sort_dates(L, reverse=False):
"""Return sorted list of dates, allowing None items (they are put at the beginning)"""
mindate = datetime.datetime(datetime.MINYEAR, 1, 1)
try:
return sorted(L, key=lambda x: x or mindate, reverse=reverse)
except:
# Helps debugging
log("sort_dates( %s )" % L)
raise
def heterogeneous_sorting_key(x):
"key to sort non homogeneous sequences"
return (float(x), "") if isinstance(x, (bool, float, int)) else (-1e34, str(x))

View File

@ -3106,7 +3106,10 @@ def formsemestre_set_elt_sem_apo():
"""Change les codes étapes du semestre indiqué.
Args: oid=formsemestre_id, value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
"""
oid = int(request.form.get("oid"))
try:
oid = int(request.form.get("oid"))
except (TypeError, ValueError) as exc:
raise ScoValueError("paramètre invalide") from exc
value = (request.form.get("value") or "").strip()
formsemestre: FormSemestre = FormSemestre.query.get_or_404(oid)
if value != formsemestre.elt_sem_apo:

View File

@ -2306,23 +2306,31 @@ def form_students_import_infos_admissions(formsemestre_id=None):
html_sco_header.sco_header(
page_title="Export données admissions (Parcoursup ou autre)",
),
"""<h2 class="formsemestre">Téléchargement des informations sur l'admission des étudiants</h2>
f"""<h2 class="formsemestre">Téléchargement des informations sur l'admission
des étudiants</h2>
<p>
<a href="import_generate_admission_sample?formsemestre_id=%(formsemestre_id)s">Exporter les informations de ScoDoc (classeur Excel)</a> (ce fichier peut être -importé après d'éventuelles modifications)
<a href="{ url_for('scolar.import_generate_admission_sample',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id )
}">Exporter les informations de ScoDoc (classeur Excel)</a> (ce fichier
peut être -importé après d'éventuelles modifications)
</p>
<p class="warning">Vous n'avez pas le droit d'importer les données</p>
"""
% {"formsemestre_id": formsemestre_id},
""",
]
return "\n".join(H) + F
# On a le droit d'importer:
H = [
html_sco_header.sco_header(page_title="Import données admissions Parcoursup"),
f"""<h2 class="formsemestre">Téléchargement des informations sur l'admission des étudiants depuis feuilles import Parcoursup</h2>
<div style="color: red">
<p>A utiliser pour renseigner les informations sur l'origine des étudiants (lycées, bac, etc). Ces informations sont facultatives mais souvent utiles pour mieux connaitre les étudiants et aussi pour effectuer des statistiques (résultats suivant le type de bac...). Les données sont affichées sur les fiches individuelles des étudiants.</p>
</div>
f"""<h2 class="formsemestre">Téléchargement des informations sur l'admission des étudiants
depuis feuilles import Parcoursup</h2>
<div style="color: red">
<p>A utiliser pour renseigner les informations sur l'origine des étudiants (lycées, bac, etc).
Ces informations sont facultatives mais souvent utiles pour mieux connaitre les étudiants
et aussi pour effectuer des statistiques (résultats suivant le type de bac...).
Les données sont affichées sur les fiches individuelles des étudiants.
</p>
</div>
<p>
Importer ici la feuille excel utilisée pour envoyer le classement Parcoursup.
Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés,

View File

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

View File

@ -20,6 +20,8 @@ import json
import requests
from types import NoneType
import pytest
from app.scodoc import sco_utils as scu
from tests.api.setup_test_api import (
@ -617,7 +619,8 @@ def test_formsemestre_programme(api_headers):
assert verify_fields(sae, MODIMPL_FIELDS)
def test_etat_evals(api_headers):
@pytest.mark.skip # XXX WIP
def test_etat_evals(api_headers): # voir si on maintient cette route ?
"""
Route : /formsemestre/<int:formsemestre_id>/etat_evals
"""