Compare commits

...

3 Commits

33 changed files with 623 additions and 638 deletions

View File

@ -38,14 +38,11 @@ import datetime
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from app import log
from app import db, log
from app.but import bulletin_but
from app.models import BulAppreciations, FormSemestre, Identite
from app.models import BulAppreciations, FormSemestre, Identite, UniteEns
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_xml
@ -202,12 +199,12 @@ def bulletin_but_xml_compat(
if e.visibulletin or version == "long":
x_eval = Element(
"evaluation",
date_debut=e.date_debut.isoformat()
if e.date_debut
else "",
date_fin=e.date_fin.isoformat()
if e.date_debut
else "",
date_debut=(
e.date_debut.isoformat() if e.date_debut else ""
),
date_fin=(
e.date_fin.isoformat() if e.date_debut else ""
),
coefficient=str(e.coefficient),
# pas les poids en XML compat
evaluation_type=str(e.evaluation_type),
@ -215,9 +212,9 @@ def bulletin_but_xml_compat(
# notes envoyées sur 20, ceci juste pour garder trace:
note_max_origin=str(e.note_max),
# --- deprecated
jour=e.date_debut.isoformat()
if e.date_debut
else "",
jour=(
e.date_debut.isoformat() if e.date_debut else ""
),
heure_debut=e.heure_debut(),
heure_fin=e.heure_fin(),
)
@ -294,17 +291,18 @@ def bulletin_but_xml_compat(
"decisions_ue"
]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee)
for ue_id in decision["decisions_ue"].keys():
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
doc.append(
Element(
"decision_ue",
ue_id=str(ue["ue_id"]),
numero=quote_xml_attr(ue["numero"]),
acronyme=quote_xml_attr(ue["acronyme"]),
titre=quote_xml_attr(ue["titre"]),
code=decision["decisions_ue"][ue_id]["code"],
ue = db.session.get(UniteEns, ue_id)
if ue:
doc.append(
Element(
"decision_ue",
ue_id=str(ue.id),
numero=quote_xml_attr(ue.numero),
acronyme=quote_xml_attr(ue.acronyme),
titre=quote_xml_attr(ue.titre or ""),
code=decision["decisions_ue"][ue_id]["code"],
)
)
)
for aut in decision["autorisations"]:
doc.append(

View File

@ -58,7 +58,6 @@ class NotesTableCompat(ResultatsSemestre):
self.moy_moy = "NA"
self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) }
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
self.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_cursus()
self._modimpls_dict_by_ue = {} # local cache
@ -217,9 +216,9 @@ class NotesTableCompat(ResultatsSemestre):
# Rangs / UEs:
for ue in ues:
group_moys_ue = self.etud_moy_ue[ue.id][group_members]
self.ue_rangs_by_group.setdefault(ue.id, {})[
group.id
] = moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
self.ue_rangs_by_group.setdefault(ue.id, {})[group.id] = (
moy_sem.comp_ranks_series(group_moys_ue * mask_inscr)
)
def get_etud_rang(self, etudid: int) -> str:
"""Le rang (classement) de l'étudiant dans le semestre.

View File

@ -79,6 +79,7 @@ class Evaluation(db.Model):
):
"""Create an evaluation. Check permission and all arguments.
Ne crée pas les poids vers les UEs.
Add to session, do not commit.
"""
if not moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied(
@ -94,6 +95,8 @@ class Evaluation(db.Model):
args["numero"] = cls.get_new_numero(moduleimpl, args["date_debut"])
#
evaluation = Evaluation(**args)
db.session.add(evaluation)
db.session.flush()
sco_cache.invalidate_formsemestre(formsemestre_id=moduleimpl.formsemestre_id)
url = url_for(
"notes.moduleimpl_status",
@ -210,9 +213,9 @@ class Evaluation(db.Model):
"visibulletin": self.visibulletin,
# Deprecated (supprimer avant #sco9.7)
"date": self.date_debut.date().isoformat() if self.date_debut else "",
"heure_debut": self.date_debut.time().isoformat()
if self.date_debut
else "",
"heure_debut": (
self.date_debut.time().isoformat() if self.date_debut else ""
),
"heure_fin": self.date_fin.time().isoformat() if self.date_fin else "",
}

View File

@ -1,8 +1,10 @@
"""ScoDoc 9 models : Modules
"""
from flask import current_app, g
from app import db
from app import models
from app.models import APO_CODE_STR_LEN
from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules
from app.scodoc import sco_utils as scu
@ -11,7 +13,7 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType
class Module(db.Model):
class Module(models.ScoDocModel):
"""Module"""
__tablename__ = "notes_modules"
@ -76,6 +78,28 @@ class Module(db.Model):
return f"""<Module{ModuleType(self.module_type or ModuleType.STANDARD).name
} id={self.id} code={self.code!r} semestre_id={self.semestre_id}>"""
@classmethod
def convert_dict_fields(cls, args: dict) -> dict:
"""Convert fields in the given dict. No other side effect.
returns: dict to store in model's db.
"""
# s'assure que ects etc est non ''
fs_empty_stored_as_nulls = {
"coefficient",
"ects",
"heures_cours",
"heures_td",
"heures_tp",
}
args_dict = {}
for key, value in args.items():
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
if key in fs_empty_stored_as_nulls and value == "":
value = None
args_dict[key] = value
return args_dict
def clone(self):
"""Create a new copy of this module."""
mod = Module(

View File

@ -50,6 +50,7 @@ from zipfile import ZipFile
import numpy as np
import pandas as pd
from app.pe import pe_semtag
from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE
import app.pe.pe_affichage as pe_affichage
from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant.
@ -96,7 +97,9 @@ class JuryPE(object):
pe_affichage.pe_print("*** Aucun étudiant diplômé")
else:
self._gen_xls_diplomes(zipfile)
self._gen_rcss()
self._gen_xls_resultats_semestres_taggues(zipfile)
self._gen_xls_semestres_taggues(zipfile)
# self._gen_xls_rcss_tags(zipfile)
# self._gen_xls_interclassements_rcss(zipfile)
# self._gen_xls_synthese_jury_par_tag(zipfile)
@ -142,12 +145,53 @@ class JuryPE(object):
output, engine="openpyxl"
) as writer:
for res_sem_tag in self.res_sems_tags.values():
onglet = res_sem_tag.get_repr()
onglet = res_sem_tag.get_repr(verbose=False)
df = res_sem_tag.df_moyennes_et_classements()
# écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True)
output.seek(0)
self.add_file_to_zip(
zipfile,
f"resultats_semestres_taggues_{self.diplome}.xlsx",
output.read(),
path="details",
)
def _gen_rcss(self):
"""Génère les RCS (attribut `rcss_jury`), combinaisons de semestres suivis par les étudiants au sens
d'un nom de RCS (par ex: '3S').
"""
pe_affichage.pe_print(
"*** Génère les RCS (différentes combinaisons de semestres) des étudiants"
)
self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome)
self.rcss_jury.cree_rcss(self.etudiants)
def _gen_xls_semestres_taggues(self, zipfile: ZipFile):
"""Génère les semestres taggués en s'appuyant sur les RCS de type Sx (pour
identifier les redoublements impactant les semestres taggués).
"""
# Génère les moyennes des RCS de type Sx
pe_affichage.pe_print("*** Calcule les moyennes de semestres = RCS de type Sx")
self.sems_tags = {}
for rcs_id, rcs in self.rcss_jury.rcss.items():
if rcs.nom.startswith("S"):
self.sems_tags[rcs_id] = pe_semtag.SemTag(rcs, self.res_sems_tags)
# Intègre le bilan des semestres taggués au zip final
output = io.BytesIO()
with pd.ExcelWriter( # pylint: disable=abstract-class-instantiated
output, engine="openpyxl"
) as writer:
for sem_tag in self.sems_tags.values():
onglet = sem_tag.get_repr(verbose=False)
df = sem_tag.df_moyennes_et_classements()
# écriture dans l'onglet
df.to_excel(writer, onglet, index=True, header=True)
output.seek(0)
self.add_file_to_zip(
zipfile,
f"semestres_taggues_{self.diplome}.xlsx",
@ -156,11 +200,7 @@ class JuryPE(object):
)
def _gen_xls_rcss_tags(self, zipfile: ZipFile):
"""Génère :
* les RCS (combinaisons de semestres suivis par les étudiants au sens
d'un aggrégat (par ex: '3S'))
* les RCS tagguées des RCS, en calculant les moyennes et les classements par tag
"""Génère les RCS tagguées des RCS, en calculant les moyennes et les classements par tag
pour chacune.
Stocke le résultat dans self.rccs_tag, un dictionnaire de
@ -183,14 +223,9 @@ class JuryPE(object):
"""
pe_affichage.pe_print(
"*** Génère les trajectoires (différentes combinaisons de semestres) des étudiants"
)
self.rcss_jury = pe_rcs.RCSsJuryPE(self.diplome)
self.rcss_jury.cree_rcss(self.etudiants)
# Génère les moyennes par tags des trajectoires
pe_affichage.pe_print("*** Calcule les moyennes par tag des RCS possibles")
# Génère les moyennes des RCS de type Sx
pe_affichage.pe_print("*** Calcule les moyennes des RCS de type Sx")
self.rcss_tags = {}
for rcs_id, rcs in self.rcss_jury.rcss.items():
@ -597,9 +632,6 @@ def compute_resultats_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
return semestres_tags
def compute_interclassements(
etudiants: EtudiantsJuryPE,
trajectoires_jury_pe: pe_rcs.RCSsJuryPE,

View File

@ -1,9 +1,11 @@
import numpy as np
import pandas as pd
from app import comp
from app.comp.moy_sem import comp_ranks_series
from app.models import UniteEns
from app.pe import pe_affichage
from app.scodoc.codes_cursus import UE_SPORT
class Moyenne:
@ -157,8 +159,9 @@ class MoyennesTag:
def __init__(
self,
tag: str,
ues: list[UniteEns],
ues: dict[int, UniteEns],
notes_ues: pd.DataFrame,
ues_inscr_parcours_df: pd.DataFrame
# notes_gen: pd.Series,
):
"""Classe centralisant la synthèse des moyennes/classements d'une série
@ -169,23 +172,41 @@ class MoyennesTag:
tag: Un tag
ues: La liste des UEs ayant servie au calcul de la moyenne
notes_ues: Les moyennes (etudid x acronymes_ues) aux différentes UEs et pour le tag
ues_inscr_parcours_df: Les inscriptions des etudid au UE
# notes_gen: Une série de notes (moyenne) sous forme d'un pd.Series() (toutes UEs confondues)
"""
self.tag = tag
"""Le tag associé aux moyennes"""
# Les UE
self.ues: dict[int, UniteEns] = {ue.id: ue for ue in ues}
self.ues: dict[int, UniteEns] = ues
"""Les UEs sur lesquelles sont calculées les moyennes"""
colonnes = list(notes_ues.columns)
acronymes = [self.ues[ue_id].acronyme for ue_id in colonnes]
assert len(set(acronymes)) == len(colonnes), \
"Deux UEs ne peuvent pas avoir le même acronyme"
acronymes: list[str] = [self.ues[ue_id].acronyme for ue_id in self.ues]
assert len(set(acronymes)) == len(
colonnes
), "Deux UEs ne peuvent pas avoir le même acronyme"
# Les inscriptions des etudids aux UEs
self.ues_inscr_parcours_df: pd.DataFrame = ues_inscr_parcours_df
"""Les inscriptions des etudids au UE en fonction de leur parcours"""
# Les coefficients à appliquer aux UEs pour la moyenne générale = ECTS
self.ects = self.ues_inscr_parcours_df.fillna(0.0) * [
ue.ects
for ue in self.ues.values() # if ue.type != UE_SPORT <= déjà supprimé
]
# Les profils d'ects (pour debug)
profils_ects = []
for val in list(self.ects.values):
if tuple(val) not in profils_ects:
profils_ects.append(tuple(val))
# Les moyennes par UE
self.notes_ues = notes_ues
self.notes_ues: pd.DataFrame = notes_ues
"""Les notes aux UEs (dataframe)"""
self.notes_ues.columns = acronymes # remplace les ue.id par leur acronyme
self.notes_ues.columns = acronymes # remplace les ue.id par leur acronyme
self.moys_ues: dict[int, pd.DataFrame] = {}
"""Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs"""
for ue in self.ues.values(): # if ue.type != UE_SPORT:
@ -193,11 +214,42 @@ class MoyennesTag:
self.moys_ues[ue.acronyme] = Moyenne(notes)
# Les moyennes générales
notes_gen = self.compute_moy_gen(self.notes_ues, self.ects)
self.notes_gen = notes_gen
"""Les notes générales (moyenne toutes UEs confonudes)"""
self.moy_gen = Moyenne(notes_gen)
"""Le dataframe retraçant les moyennes/classements/statistiques général"""
pe_affichage.pe_print(f"> MoyTag pour {tag} avec")
pe_affichage.pe_print(f" - ues={acronymes}")
pe_affichage.pe_print(f" - ects={profils_ects}")
def __eq__(self, other):
"""Egalité de deux MoyenneTag lorsque leur tag sont identiques"""
return self.tag == other.tag
def compute_moy_gen(
self, moy_ues: pd.DataFrame, coeff_ues: pd.DataFrame
) -> pd.Series:
"""Calcule la moyenne générale (toutes UE confondus)
pour le tag considéré, en pondérant les notes obtenues au UE
par les crédits ECTS.
Args:
moy_ues: Les moyennes etudids x acronymes_ues
coeff_ues: Les coeff etudids x ueids
"""
# Calcule la moyenne générale dans le semestre (pondérée par le ECTS)
try:
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
moy_ues,
coeff_ues,
# formation_id=self.formsemestre.formation_id,
skip_empty_ues=True,
)
except TypeError as e:
raise TypeError("Pb dans le calcul de la moyenne toutes UEs confondues")
return moy_gen_tag

View File

@ -94,7 +94,7 @@ class RCSTag(TableTag):
self.etuds = nt.etuds
# assert self.etuds == trajectoire.suivi # manque-t-il des étudiants ?
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
self.etats_civils = {etud.etudid: etud.nomprenom for etud in self.etuds}
self.tags_sorted = self.do_taglist()
"""Tags extraits de tous les semestres"""
@ -102,7 +102,7 @@ class RCSTag(TableTag):
self.notes_cube = self.compute_notes_cube()
"""Cube de notes"""
etudids = list(self.etudiants.keys())
etudids = list(self.etats_civils.keys())
self.notes = compute_tag_moy(self.notes_cube, etudids, self.tags_sorted)
"""Calcul les moyennes par tag sous forme d'un dataframe"""

View File

@ -70,19 +70,15 @@ class ResSemTag(TableTag):
self.formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Le nom du res_semestre taggué
self.nom = self.get_repr(mode="long")
self.nom = self.get_repr(verbose=True)
pe_affichage.pe_print(
f"--> Résultats de Semestre taggués {self.nom}"
)
pe_affichage.pe_print(f"--> Résultats de semestre taggués {self.nom}")
# Les résultats du semestre
self.nt = load_formsemestre_results(self.formsemestre)
# Les étudiants
self.etuds = self.nt.etuds
self.etudiants = {etud.etudid: etud.etat_civil for etud in self.etuds}
self.etudids = list(self.etudiants.keys())
# Les étudiants (etuds, états civils & etudis)
self.add_etuds(self.nt.etuds)
# Les notes, les modules implémentés triés, les étudiants, les coeffs,
# récupérés notamment de py:mod:`res_but`
@ -109,12 +105,17 @@ class ResSemTag(TableTag):
for tag in tags_dict["personnalises"]:
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
infos_tag = tags_dict["personnalises"][tag]
infos_tag = tags_dict["personnalises"][tag]
moy_ues_tag = self.compute_moy_ues_tag(infos_tag)
# moy_gen_tag = self.compute_moy_gen_tag(moy_ues_tag)
ues_dict = {ue.id: ue for ue in ues_hors_sport}
self.moyennes_tags[tag] = MoyennesTag(
tag, ues_hors_sport, moy_ues_tag # moy_gen_tag
tag,
ues_dict,
moy_ues_tag,
self.ues_inscr_parcours_df
# moy_gen_tag
)
# Ajoute les d'UE moyennes générales de BUT pour le semestre considéré
@ -128,8 +129,10 @@ class ResSemTag(TableTag):
)
# moy_ues = self.nt.etud_moy_ue[ue_id]
# moy_gen_but = self.nt.etud_moy_gen
ues_dict = {ue.id: ue for ue in ues_hors_sport}
self.moyennes_tags["but"] = MoyennesTag(
"but", ues_hors_sport, df_ues #, moy_gen_but
"but", ues_dict, df_ues, self.ues_inscr_parcours_df # , moy_gen_but
)
self.tags_sorted = self.get_all_tags()
@ -144,11 +147,11 @@ class ResSemTag(TableTag):
# f" => Traitement des tags {', '.join(self.tags_sorted)}"
# )
def get_repr(self, mode="long"):
def get_repr(self, verbose=False):
"""Nom affiché pour le semestre taggué"""
if mode == "short":
if verbose:
return f"{self.formsemestre} ({self.formsemestre_id})"
else: # mode == "long"
else:
return pe_etudiant.nom_semestre_etape(self.formsemestre, avec_fid=True)
def compute_moy_ues_tag(self, info_tag: dict[int, dict]) -> pd.DataFrame:
@ -192,25 +195,6 @@ class ResSemTag(TableTag):
)
return moyennes_ues_tag
def compute_moy_gen_tag(self, moy_ues_tag: pd.DataFrame) -> pd.Series:
"""Calcule la moyenne générale (toutes UE confondus)
pour le tag considéré, en les pondérant par les crédits ECTS.
"""
# Les ects
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
ue.ects for ue in self.ues if ue.type != UE_SPORT
]
# Calcule la moyenne générale dans le semestre (pondérée par le ECTS)
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
moy_ues_tag,
ects,
formation_id=self.formsemestre.formation_id,
skip_empty_ues=True,
)
return moy_gen_tag
def _get_tags_dict(self):
"""Renvoie les tags personnalisés (déduits des modules du semestre)
et les tags automatiques ('but'), et toutes leurs informations,
@ -272,8 +256,6 @@ class ResSemTag(TableTag):
raise ScoValueError(message)
def get_moduleimpl(modimpl_id) -> dict:
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
modimpl = db.session.get(ModuleImpl, modimpl_id)

View File

@ -49,7 +49,7 @@ from app.pe.pe_moytag import MoyennesTag
class SemTag(TableTag):
def __init__(self, rcs: RCS, semestres_taggues: dict[int, ResSemTag]):
def __init__(self, rcs: RCS, res_sems_tags: dict[int, ResSemTag]):
"""Calcule les moyennes/classements par tag à un RCS d'un seul semestre
(ici semestre) de type 'Sx' (par ex. 'S1', 'S2', ...) :
@ -62,7 +62,7 @@ class SemTag(TableTag):
Args:
rcs: Un RCS (identifié par un nom et l'id de son semestre terminal)
semestres_taggues: Les données sur les semestres taggués
res_sems_tags: Les données sur les résultats des semestres taggués
"""
TableTag.__init__(self)
@ -88,27 +88,20 @@ class SemTag(TableTag):
self.semestres_aggreges = rcs.semestres_aggreges
"""Les semestres aggrégés"""
self.semestres_tags_aggreges = {}
"""Les semestres tags associés aux semestres aggrégés"""
self.res_sems_tags = {}
"""Les résultats des semestres taggués (limités aux semestres aggrégés)"""
try:
for frmsem_id in self.semestres_aggreges:
self.semestres_tags_aggreges[frmsem_id] = semestres_taggues[frmsem_id]
self.res_sems_tags[frmsem_id] = res_sems_tags[frmsem_id]
except:
raise ValueError("Semestres taggués manquants")
raise ValueError("Résultats des semestres taggués manquants")
# Les données des étudiants
self.etuds = nt.etuds
"""Les étudiants"""
self.etudids = [etud.etudid for etud in self.etuds]
"""Les etudids"""
self.etats_civils = {
etudid: self.etuds[etudid].etat_civil for etudid in self.etudids
}
"""Les états civils"""
# Les étudiants (etuds, états civils & etudis)
self.add_etuds(nt.etuds)
# Les tags
self.tags_sorted = self.comp_tags_list()
"""Tags extraits du semestre terminal de l'aggrégat"""
"""Tags (extraits uniquement du semestre terminal de l'aggrégat)"""
# Les UEs
self.ues = self.comp_ues(tag="but")
@ -116,20 +109,33 @@ class SemTag(TableTag):
"""UEs extraites du semestre terminal de l'aggrégat (avec
check de concordance sur les UE des semestres_aggrégés)"""
# Les inscriptions aux UEs
self.ues_inscr_parcours_df = self.comp_ues_inscr_parcours(tag="but")
"""Les inscriptions aux UEs (extraites uniquement du semestre terminal)"""
self.moyennes_tags: dict[str, MoyennesTag] = {}
"""Moyennes/classements par tag (qu'ils soient personnalisés ou automatiques)"""
self.notes: dict[str, pd.DataFrame] = {}
"""Les notes aux différents tags"""
self.moyennes_tags: dict[str, pd.DataFrame] = {}
"""Les notes aux UEs dans différents tags"""
# Masque des inscriptions
inscr_mask = self.ues_inscr_parcours_df.to_numpy()
for tag in self.tags_sorted:
# Cube de note
notes_cube = self.compute_notes_ues_cube(tag, self.acronymes_ues)
notes_cube = self.compute_notes_ues_cube(tag, self.acronymes_ues_sorted)
# Calcule des moyennes sous forme d'un dataframe"""
self.notes[tag] = compute_notes_ues(notes_cube, self.etudids, self.acronymes_ues)
moys_ues = compute_notes_ues(
notes_cube,
self.etudids,
self.acronymes_ues_sorted,
inscr_mask,
)
# Les moyennes
self.moyennes_tags[tag] = MoyennesTag(tag, self.notes[tag])
self.moyennes_tags[tag] = MoyennesTag(tag,
self.ues,
moys_ues,
self.ues_inscr_parcours_df)
def __eq__(self, other):
"""Egalité de 2 RCS taggués sur la base de leur identifiant"""
@ -147,7 +153,7 @@ class SemTag(TableTag):
# Index du cube (etudids -> dim 0, ues -> dim 1, semestres -> dim2)
etudids = [etud.etudid for etud in self.etuds]
# acronymes_ues = sorted([ue.acronyme for ue in self.ues.values()])
semestres_id = list(self.semestres_tags_aggreges.keys())
semestres_id = list(self.res_sems_tags.keys())
dfs = {}
@ -156,10 +162,12 @@ class SemTag(TableTag):
df = pd.DataFrame(np.nan, index=etudids, columns=acronymes_ues_sorted)
# Charge les notes du semestre tag
sem_tag = self.semestres_tags_aggreges[frmsem_id]
sem_tag = self.res_sems_tags[frmsem_id]
moys_tag = sem_tag.moyennes_tags[tag]
notes = moys_tag.notes_ues # dataframe etudids x ues
acronymes_ues_sem = list(notes.columns) # les acronymes des UEs du semestre tag
notes = moys_tag.notes_ues # dataframe etudids x ues
acronymes_ues_sem = list(
notes.columns
) # les acronymes des UEs du semestre tag
# UEs communes à celles du SemTag (celles du dernier semestre du RCS)
ues_communes = list(set(acronymes_ues_sorted) & set(acronymes_ues_sem))
@ -168,7 +176,9 @@ class SemTag(TableTag):
etudids_communs = df.index.intersection(notes.index)
# Recopie
df.loc[etudids_communs, ues_communes] = notes.loc[etudids_communs, ues_communes]
df.loc[etudids_communs, ues_communes] = notes.loc[
etudids_communs, ues_communes
]
# Supprime tout ce qui n'est pas numérique
for col in df.columns:
@ -182,7 +192,7 @@ class SemTag(TableTag):
etudids_x_ues_x_semestres = np.stack(semestres_x_etudids_x_ues, axis=-1)
return etudids_x_ues_x_semestres
def comp_tags_list(self):
def comp_tags_list(self) -> list[str]:
"""Récupère les tag du semestre taggué associé au semestre final du RCS
Returns:
@ -190,7 +200,7 @@ class SemTag(TableTag):
"""
tags = []
dernier_frmid = self.formsemestre_terminal.formsemestre_id
dernier_semestre_tag = self.semestres_tags_aggreges[dernier_frmid]
dernier_semestre_tag = self.res_sems_tags[dernier_frmid]
tags = dernier_semestre_tag.tags_sorted
pe_affichage.pe_print(f"* Tags : {', '.join(tags)}")
return tags
@ -203,12 +213,29 @@ class SemTag(TableTag):
Un dictionnaire donnant les UEs
"""
dernier_frmid = self.formsemestre_terminal.formsemestre_id
dernier_semestre_tag = self.semestres_tags_aggreges[dernier_frmid]
dernier_semestre_tag = self.res_sems_tags[dernier_frmid]
moy_tag = dernier_semestre_tag.moyennes_tags[tag]
return moy_tag.ues # les UEs
def comp_ues_inscr_parcours(self, tag="but") -> pd.DataFrame:
"""Récupère les informations d'inscription des étudiants aux UEs : ne
conserve que les UEs du semestre terminal (pour les redoublants)
def compute_notes_ues(set_cube: np.array, etudids: list, acronymes_ues: list):
Returns:
Un dataFrame etudids x UE indiquant si un étudiant est inscrit à une UE
"""
dernier_frmid = self.formsemestre_terminal.formsemestre_id
dernier_semestre_tag = self.res_sems_tags[dernier_frmid]
moy_tag = dernier_semestre_tag.moyennes_tags[tag]
return moy_tag.ues_inscr_parcours_df
def compute_notes_ues(
set_cube: np.array,
etudids: list,
acronymes_ues: list,
inscr_mask: np.array,
):
"""Calcule la moyenne par UEs à un tag donné en prenant la note maximum (UE
par UE) obtenue par un étudiant à un semestre.
@ -217,18 +244,27 @@ def compute_notes_ues(set_cube: np.array, etudids: list, acronymes_ues: list):
(semestre_ids x etudids x UEs), des floats avec des NaN
etudids: liste des étudiants (dim. 0 du cube)
acronymes_ues: liste des acronymes des ues (dim. 1 du cube)
inscr_mask: masque etudids x UE traduisant les inscriptions des
étudiants aux UE (du semestre terminal)
Returns:
Un DataFrame avec pour columns les moyennes par ues,
et pour rows les etudid
"""
nb_etuds, nb_ues, nb_semestres = set_cube.shape
nb_etuds_mask, nb_ues_mask = inscr_mask.shape
assert nb_etuds == len(etudids)
assert nb_ues == len(acronymes_ues)
assert nb_etuds == nb_etuds_mask
assert nb_ues == nb_ues_mask
# Quelles entrées du cube contiennent des notes ?
mask = ~np.isnan(set_cube)
# Enlève les NaN du cube pour les entrées manquantes
# Entrées à garder dans le cube en fonction du mask d'inscription
inscr_mask_3D = np.stack([inscr_mask]*nb_semestres, axis=-1)
set_cube = set_cube*inscr_mask_3D
# Enlève les NaN du cube pour les entrées manquantes : NaN -> -1.0
set_cube_no_nan = np.nan_to_num(set_cube, nan=-1.0)
# Les moyennes par ues

View File

@ -39,6 +39,8 @@ Created on Thu Sep 8 09:36:33 2016
import pandas as pd
from app.models import Identite
TAGS_RESERVES = ["but"]
@ -47,7 +49,23 @@ class TableTag(object):
"""Classe centralisant différentes méthodes communes aux
SemestreTag, TrajectoireTag, AggregatInterclassTag
"""
pass
# Les étudiants
self.etuds: list[Identite] = None # A venir
"""Les étudiants"""
self.etats_civils: dict[int, Identite] = None
"""Les états civils"""
self.etudids: list[int] = None
"""Les etudids"""
def add_etuds(self, etuds: list[Identite]):
"""Mémorise les informations sur les étudiants
Args:
etuds: la liste des identités de l'étudiant
"""
self.etuds = etuds
self.etats_civils = {etud.etudid: etud.etat_civil for etud in self.etuds}
self.etudids = list(self.etats_civils.keys())
def get_all_tags(self):
"""Liste des tags de la table, triée par ordre alphabétique,
@ -70,15 +88,23 @@ class TableTag(object):
Le dataframe des notes et des classements
"""
etudiants = self.etudiants
etudiants = self.etats_civils
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]
for acronyme in moy_tag.moys_ues:
moy = moy_tag.moys_ues[acronyme] # une moyenne
df = df.join(moy.synthese["notes"].rename(f"Moy {tag}-{acronyme}"))
df = df.join(
moy.synthese["classements"].rename(f"Class {tag}-{acronyme}")
)
moy_gen = moy_tag.moy_gen
df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag}"))
df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag}"))
df = df.join(moy_gen.synthese["notes"].rename(f"Moy {tag} (gen)"))
df = df.join(moy_gen.synthese["classements"].rename(f"Class {tag} (gen)"))
df.sort_values(by=['nom'])
return df

View File

@ -409,6 +409,7 @@ class CursusBUT(TypeCursus):
APC_SAE = True
USE_REFERENTIEL_COMPETENCES = True
ALLOWED_UE_TYPES = [UE_STANDARD, UE_SPORT]
ECTS_DIPLOME = 180
register_cursus(CursusBUT())

View File

@ -44,13 +44,15 @@ import random
from collections import OrderedDict
from xml.etree import ElementTree
import json
from typing import Any
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
from openpyxl.utils import get_column_letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from reportlab.platypus import Paragraph, Spacer
from reportlab.platypus import Table, KeepInFrame
from reportlab.lib.colors import Color
from reportlab.lib import styles
from reportlab.lib.units import inch, cm, mm
from reportlab.rl_config import defaultPageSize # pylint: disable=no-name-in-module
from reportlab.lib.units import cm
from app.scodoc import html_sco_header
from app.scodoc import sco_utils as scu
@ -62,16 +64,32 @@ from app.scodoc.sco_pdf import SU
from app import log, ScoDocJSONEncoder
def mark_paras(L, tags) -> list[str]:
"""Put each (string) element of L between <tag>...</tag>,
def mark_paras(items: list[Any], tags: list[str]) -> list[str]:
"""Put each string element of items between <tag>...</tag>,
for each supplied tag.
Leave non string elements untouched.
"""
for tag in tags:
start = "<" + tag + ">"
end = "</" + tag.split()[0] + ">"
L = [(start + (x or "") + end) if isinstance(x, str) else x for x in L]
return L
items = [(start + (x or "") + end) if isinstance(x, str) else x for x in items]
return items
def add_query_param(url: str, key: str, value: str) -> str:
"add parameter key=value to the given URL"
# Parse the URL
parsed_url = urlparse(url)
# Parse the query parameters
query_params = parse_qs(parsed_url.query)
# Add or update the query parameter
query_params[key] = [value]
# Encode the query parameters
encoded_query_params = urlencode(query_params, doseq=True)
# Construct the new URL
new_url_parts = parsed_url._replace(query=encoded_query_params)
new_url = urlunparse(new_url_parts)
return new_url
class DEFAULT_TABLE_PREFERENCES(object):
@ -477,13 +495,15 @@ class GenTable:
H.append('<span class="gt_export_icons">')
if self.xls_link:
H.append(
' <a href="%s&fmt=xls">%s</a>' % (self.base_url, scu.ICON_XLS)
f""" <a href="{add_query_param(self.base_url, "fmt", "xls")
}">{scu.ICON_XLS}</a>"""
)
if self.xls_link and self.pdf_link:
H.append("&nbsp;")
if self.pdf_link:
H.append(
' <a href="%s&fmt=pdf">%s</a>' % (self.base_url, scu.ICON_PDF)
f""" <a href="{add_query_param(self.base_url, "fmt", "pdf")
}">{scu.ICON_PDF}</a>"""
)
H.append("</span>")
H.append("</p>")
@ -582,9 +602,11 @@ class GenTable:
for line in data_list:
Pt.append(
[
Paragraph(SU(str(x)), CellStyle)
if (not isinstance(x, Paragraph))
else x
(
Paragraph(SU(str(x)), CellStyle)
if (not isinstance(x, Paragraph))
else x
)
for x in line
]
)

View File

@ -109,7 +109,7 @@ def sidebar_common():
{sidebar_dept()}
<h2 class="insidebar">Scolarité</h2>
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br>
<a href="{scu.NotesURL()}" class="sidebar">Programmes</a> <br>
<a href="{scu.NotesURL()}" class="sidebar">Formations</a> <br>
"""
]
if current_user.has_permission(Permission.AbsChange):

View File

@ -114,7 +114,7 @@ def index_html(showcodes=0, showsemtable=0):
# aucun semestre courant: affiche aide
H.append(
"""<h2 class="listesems">Aucune session en cours !</h2>
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Programmes</a>,
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Formations</a>,
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
</p><p>
, en bas de page, suivez le lien
@ -336,15 +336,15 @@ def _style_sems(sems):
else:
sem["semestre_id_n"] = sem["semestre_id"]
# pour édition codes Apogée:
sem[
"_etapes_apo_str_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
sem[
"_elt_annee_apo_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
sem[
"_elt_sem_apo_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
sem["_etapes_apo_str_td_attrs"] = (
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
)
sem["_elt_annee_apo_td_attrs"] = (
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_annee_apo']}" """
)
sem["_elt_sem_apo_td_attrs"] = (
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
)
def delete_dept(dept_id: int) -> str:

View File

@ -412,7 +412,7 @@ def module_move(module_id, after=0, redirect=True):
db.session.add(neigh)
db.session.commit()
module.formation.invalidate_cached_sems()
# redirect to ue_list page:
# redirect to ue_table page:
if redirect:
return flask.redirect(
url_for(
@ -454,7 +454,7 @@ def ue_move(ue_id, after=0, redirect=1):
db.session.commit()
ue.formation.invalidate_cached_sems()
# redirect to ue_list page
# redirect to ue_table page
if redirect:
return flask.redirect(
url_for(

View File

@ -106,9 +106,9 @@ def do_module_create(args) -> int:
if int(args.get("semestre_id", 0)) != ue.semestre_idx:
raise ScoValueError("Formation incompatible: contacter le support ScoDoc")
# create
cnx = ndb.GetDBConnexion()
module_id = _moduleEditor.create(cnx, args)
log(f"do_module_create: created {module_id} with {args}")
module = Module.create_from_dict(args)
db.session.commit()
log(f"do_module_create: created {module.id} with {args}")
# news
ScolarNews.add(
@ -117,7 +117,7 @@ def do_module_create(args) -> int:
text=f"Modification de la formation {formation.acronyme}",
)
formation.invalidate_cached_sems()
return module_id
return module.id
def module_create(
@ -666,7 +666,7 @@ def module_edit(
"explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage",
"type": "int",
"default": default_num,
"allow_null": False,
"allow_null": True,
},
),
]
@ -811,6 +811,10 @@ def module_edit(
)
)
else:
if isinstance(tf[2]["numero"], str):
tf[2]["numero"] = tf[2]["numero"].strip()
if not isinstance(tf[2]["numero"], int) and not tf[2]["numero"]:
tf[2]["numero"] = tf[2]["numero"] or default_num
if create:
if not matiere_id:
# formulaire avec choix UE de rattachement

View File

@ -766,7 +766,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
"libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js",
],
page_title=f"Programme {formation.acronyme} v{formation.version}",
page_title=f"Formation {formation.acronyme} v{formation.version}",
),
f"""<h2>{formation.html()} {lockicon}
</h2>
@ -888,7 +888,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
H.append(
f"""
<div class="formation_ue_list">
<div class="ue_list_tit">Programme pédagogique:</div>
<div class="ue_list_tit">Formation (programme pédagogique):</div>
<form>
<input type="checkbox" class="sco_tag_checkbox"
{'checked' if show_tags else ''}
@ -1054,7 +1054,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
)
# <li>(debug) <a class="stdlink" href="check_form_integrity?formation_id=%(formation_id)s">Vérifier cohérence</a></li>
warn, _ = sco_formsemestre_validation.check_formation_ues(formation_id)
warn, _ = sco_formsemestre_validation.check_formation_ues(formation)
H.append(warn)
H.append(html_sco_header.sco_footer())

View File

@ -30,7 +30,7 @@
import xml.dom.minidom
import flask
from flask import flash, g, url_for
from flask import flash, g, request, url_for
from flask_login import current_user
import app.scodoc.sco_utils as scu
@ -495,7 +495,7 @@ def formation_list_table() -> GenTable:
returns a table
"""
formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
title = "Programmes pédagogiques"
title = "Formations (programmes pédagogiques)"
lockicon = scu.icontag(
"lock32_img", title="Comporte des semestres verrouillés", border="0"
)
@ -627,7 +627,7 @@ def formation_list_table() -> GenTable:
html_class="formation_list_table table_leftalign",
html_with_td_classes=True,
html_sortable=True,
base_url="{request.base_url}?formation_id={formation_id}",
base_url=f"{request.base_url}",
page_title=title,
pdf_title=title,
preferences=sco_preferences.SemPreferences(),

View File

@ -304,12 +304,16 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{
"input_type": "text_suggest",
"size": 50,
"title": "(Co-)Directeur(s) des études"
if index
else "Directeur des études",
"explanation": "(facultatif) taper le début du nom et choisir dans le menu"
if index
else "(obligatoire) taper le début du nom et choisir dans le menu",
"title": (
"(Co-)Directeur(s) des études"
if index
else "Directeur des études"
),
"explanation": (
"(facultatif) taper le début du nom et choisir dans le menu"
if index
else "(obligatoire) taper le début du nom et choisir dans le menu"
),
"allowed_values": allowed_user_names,
"allow_null": index, # > 0, # il faut au moins un responsable de semestre
"text_suggest_options": {
@ -356,9 +360,11 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"title": "Semestre dans la formation",
"allowed_values": semestre_id_list,
"labels": semestre_id_labels,
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
if is_apc
else "",
"explanation": (
"en BUT, on ne peut pas modifier le semestre après création"
if is_apc
else ""
),
"attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
},
),
@ -1636,13 +1642,13 @@ def formsemestre_change_publication_bul(
def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
"""Changement manuel des coefficients des UE capitalisées."""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
if not ok:
return err
footer = html_sco_header.sco_footer()
help = """<p class="help">
help_msg = """<p class="help">
Seuls les modules ont un coefficient. Cependant, il est nécessaire d'affecter un coefficient aux UE capitalisée pour pouvoir les prendre en compte dans la moyenne générale.
</p>
<p class="help">ScoDoc calcule normalement le coefficient d'une UE comme la somme des
@ -1665,17 +1671,16 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
"""
H = [
html_sco_header.html_sem_header("Coefficients des UE du semestre"),
help,
help_msg,
]
#
ues, modimpls = _get_sem_ues_modimpls(formsemestre_id)
ues, modimpls = _get_sem_ues_modimpls(formsemestre)
sum_coefs_by_ue_id = {}
for ue in ues:
ue["sum_coefs"] = sum(
[
mod["module"]["coefficient"]
for mod in modimpls
if mod["module"]["ue_id"] == ue["ue_id"]
]
sum_coefs_by_ue_id[ue.id] = sum(
modimpl.module.coefficient
for modimpl in modimpls
if modimpl.module.ue_id == ue.id
)
cnx = ndb.GetDBConnexion()
@ -1684,20 +1689,20 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
form = [("formsemestre_id", {"input_type": "hidden"})]
for ue in ues:
coefs = sco_formsemestre.formsemestre_uecoef_list(
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue.id}
)
if coefs:
initvalues["ue_" + str(ue["ue_id"])] = coefs[0]["coefficient"]
initvalues["ue_" + str(ue.id)] = coefs[0]["coefficient"]
else:
initvalues["ue_" + str(ue["ue_id"])] = "auto"
initvalues["ue_" + str(ue.id)] = "auto"
descr = {
"size": 10,
"title": ue["acronyme"],
"explanation": "somme coefs modules = %s" % ue["sum_coefs"],
"title": ue.acronyme,
"explanation": f"somme coefs modules = {sum_coefs_by_ue_id[ue.id]}",
}
if ue["ue_id"] == err_ue_id:
if ue.id == err_ue_id:
descr["dom_id"] = "erroneous_ue"
form.append(("ue_" + str(ue["ue_id"]), descr))
form.append(("ue_" + str(ue.id), descr))
tf = TrivialFormulator(
request.base_url,
@ -1722,12 +1727,12 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
# 1- supprime les coef qui ne sont plus forcés
# 2- modifie ou cree les coefs
ue_deleted = []
ue_modified = []
ue_modified: list[tuple[UniteEns, float]] = []
msg = []
for ue in ues:
val = tf[2]["ue_" + str(ue["ue_id"])]
val = tf[2]["ue_" + str(ue.id)]
coefs = sco_formsemestre.formsemestre_uecoef_list(
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]}
cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue.id}
)
if val == "" or val == "auto":
# supprime ce coef (il sera donc calculé automatiquement)
@ -1737,13 +1742,11 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
try:
val = float(val)
if (not coefs) or (coefs[0]["coefficient"] != val):
ue["coef"] = val
ue_modified.append(ue)
except:
ue_modified.append((ue, val))
except ValueError:
ok = False
msg.append(
"valeur invalide (%s) pour le coefficient de l'UE %s"
% (val, ue["acronyme"])
f"valeur invalide ({val}) pour le coefficient de l'UE {ue.acronyme}"
)
if not ok:
@ -1755,26 +1758,24 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
)
# apply modifications
for ue in ue_modified:
for ue, val in ue_modified:
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
cnx, formsemestre_id, ue["ue_id"], ue["coef"]
cnx, formsemestre_id, ue.id, val
)
for ue in ue_deleted:
sco_formsemestre.do_formsemestre_uecoef_delete(
cnx, formsemestre_id, ue["ue_id"]
)
sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue.id)
if ue_modified or ue_deleted:
message = ["""<h3>Modification effectuées</h3>"""]
if ue_modified:
message.append("""<h4>Coefs modifiés dans les UE:<h4><ul>""")
for ue in ue_modified:
message.append("<li>%(acronyme)s : %(coef)s</li>" % ue)
for ue, val in ue_modified:
message.append(f"<li>{ue.acronyme} : {val}</li>")
message.append("</ul>")
if ue_deleted:
message.append("""<h4>Coefs supprimés dans les UE:<h4><ul>""")
for ue in ue_deleted:
message.append("<li>%(acronyme)s</li>" % ue)
message.append(f"<li>{ue.acronyme}</li>")
message.append("</ul>")
else:
message = ["""<h3>Aucune modification</h3>"""]
@ -1792,21 +1793,19 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
"""
def _get_sem_ues_modimpls(formsemestre_id, modimpls=None):
def _get_sem_ues_modimpls(
formsemestre: FormSemestre,
) -> tuple[list[UniteEns], list[ModuleImpl]]:
"""Get liste des UE du semestre (à partir des moduleimpls)
(utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict())
"""
if modimpls is None:
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
uedict = {}
modimpls = formsemestre.modimpls.all()
for modimpl in modimpls:
mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
modimpl["module"] = mod
if not mod["ue_id"] in uedict:
ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
uedict[ue["ue_id"]] = ue
if not modimpl.module.ue_id in uedict:
uedict[modimpl.module.ue.id] = modimpl.module.ue
ues = list(uedict.values())
ues.sort(key=lambda u: u["numero"])
ues.sort(key=lambda u: u.numero)
return ues, modimpls

View File

@ -305,18 +305,15 @@ def do_formsemestre_inscription_with_modules(
# 2- inscrit aux groupes
for group_id in group_ids:
if group_id and group_id not in gdone:
group = GroupDescr.query.get_or_404(group_id)
_ = GroupDescr.query.get_or_404(group_id)
sco_groups.set_group(etudid, group_id)
gdone[group_id] = 1
# Inscription à tous les modules de ce semestre
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
)
for mod in modimpls:
if mod["ue"]["type"] != UE_SPORT:
for modimpl in formsemestre.modimpls:
if modimpl.module.ue.type != UE_SPORT:
sco_moduleimpl.do_moduleimpl_inscription_create(
{"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid},
{"moduleimpl_id": modimpl.id, "etudid": etudid},
formsemestre_id=formsemestre_id,
)
# Mise à jour des inscriptions aux parcours:
@ -531,19 +528,17 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
if not sem["etat"]:
raise ScoValueError("Modification impossible: semestre verrouille")
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
etud = Identite.get_etud(etudid)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
footer = html_sco_header.sco_footer()
H = [
html_sco_header.sco_header()
+ "<h2>Inscription de %s aux modules de %s (%s - %s)</h2>"
% (etud["nomprenom"], sem["titre_num"], sem["date_debut"], sem["date_fin"])
html_sco_header.sco_header(),
f"""<h2>Inscription de {etud.nomprenom} aux modules de {formsemestre.titre_mois()}</h2>""",
]
# Cherche les moduleimpls et les inscriptions
mods = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
inscr = sco_moduleimpl.do_moduleimpl_inscription_list(etudid=etudid)
# Formulaire
modimpls_by_ue_ids = collections.defaultdict(list) # ue_id : [ moduleimpl_id ]
@ -551,26 +546,26 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
ues = []
ue_ids = set()
initvalues = {}
for mod in mods:
ue_id = mod["ue"]["ue_id"]
for modimpl in formsemestre.modimpls:
ue_id = modimpl.module.ue.id
if not ue_id in ue_ids:
ues.append(mod["ue"])
ues.append(modimpl.module.ue)
ue_ids.add(ue_id)
modimpls_by_ue_ids[ue_id].append(mod["moduleimpl_id"])
modimpls_by_ue_ids[ue_id].append(modimpl.id)
modimpls_by_ue_names[ue_id].append(
"%s %s" % (mod["module"]["code"] or "", mod["module"]["titre"] or "")
f"{modimpl.module.code or ''} {modimpl.module.titre or ''}"
)
vals = scu.get_request_args()
if not vals.get("tf_submitted", False):
# inscrit ?
for ins in inscr:
if ins["moduleimpl_id"] == mod["moduleimpl_id"]:
key = "moduleimpls_%s" % ue_id
if ins["moduleimpl_id"] == modimpl.id:
key = f"moduleimpls_{ue_id}"
if key in initvalues:
initvalues[key].append(str(mod["moduleimpl_id"]))
initvalues[key].append(str(modimpl.id))
else:
initvalues[key] = [str(mod["moduleimpl_id"])]
initvalues[key] = [str(modimpl.id)]
break
descr = [
@ -578,10 +573,10 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
("etudid", {"input_type": "hidden"}),
]
for ue in ues:
ue_id = ue["ue_id"]
ue_descr = ue["acronyme"]
if ue["type"] != UE_STANDARD:
ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue["type"]]
ue_id = ue.id
ue_descr = ue.acronyme
if ue.type != UE_STANDARD:
ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue.type]
ue_status = nt.get_etud_ue_status(etudid, ue_id)
if ue_status and ue_status["is_capitalized"]:
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
@ -606,7 +601,7 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
)
descr.append(
(
"moduleimpls_%s" % ue_id,
f"moduleimpls_{ue_id}",
{
"input_type": "checkbox",
"title": "",
@ -654,112 +649,98 @@ function chkbx_select(field_id, state) {
"""
)
return "\n".join(H) + "\n" + tf[1] + footer
elif tf[0] == -1:
if tf[0] == -1:
return flask.redirect(
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
else:
# Inscriptions aux modules choisis
# il faut desinscrire des modules qui ne figurent pas
# et inscrire aux autres, sauf si deja inscrit
a_desinscrire = {}.fromkeys([x["moduleimpl_id"] for x in mods])
insdict = {}
for ins in inscr:
insdict[ins["moduleimpl_id"]] = ins
for ue in ues:
ue_id = ue["ue_id"]
for moduleimpl_id in [int(x) for x in tf[2]["moduleimpls_%s" % ue_id]]:
if moduleimpl_id in a_desinscrire:
del a_desinscrire[moduleimpl_id]
# supprime ceux auxquel pas inscrit
moduleimpls_a_desinscrire = list(a_desinscrire.keys())
for moduleimpl_id in moduleimpls_a_desinscrire:
if moduleimpl_id not in insdict:
# Inscriptions aux modules choisis
# il faut desinscrire des modules qui ne figurent pas
# et inscrire aux autres, sauf si deja inscrit
a_desinscrire = {}.fromkeys([x.id for x in formsemestre.modimpls])
insdict = {}
for ins in inscr:
insdict[ins["moduleimpl_id"]] = ins
for ue in ues:
for moduleimpl_id in [int(x) for x in tf[2][f"moduleimpls_{ue.id}"]]:
if moduleimpl_id in a_desinscrire:
del a_desinscrire[moduleimpl_id]
# supprime ceux auxquel pas inscrit
moduleimpls_a_desinscrire = list(a_desinscrire.keys())
for moduleimpl_id in moduleimpls_a_desinscrire:
if moduleimpl_id not in insdict:
del a_desinscrire[moduleimpl_id]
a_inscrire = set()
for ue in ues:
ue_id = ue["ue_id"]
a_inscrire.update(
int(x) for x in tf[2]["moduleimpls_%s" % ue_id]
) # conversion en int !
# supprime ceux auquel deja inscrit:
for ins in inscr:
if ins["moduleimpl_id"] in a_inscrire:
a_inscrire.remove(ins["moduleimpl_id"])
# dict des modules:
modsdict = {}
for mod in mods:
modsdict[mod["moduleimpl_id"]] = mod
#
if (not a_inscrire) and (not a_desinscrire):
H.append(
"""<h3>Aucune modification à effectuer</h3>
<p><a class="stdlink" href="%s">retour à la fiche étudiant</a></p>
"""
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
return "\n".join(H) + footer
H.append("<h3>Confirmer les modifications:</h3>")
if a_desinscrire:
H.append(
"<p>%s va être <b>désinscrit%s</b> des modules:<ul><li>"
% (etud["nomprenom"], etud["ne"])
)
H.append(
"</li><li>".join(
[
"%s (%s)"
% (
modsdict[x]["module"]["titre"],
modsdict[x]["module"]["code"] or "(module sans code)",
)
for x in a_desinscrire
]
)
+ "</p>"
)
H.append("</li></ul>")
if a_inscrire:
H.append(
"<p>%s va être <b>inscrit%s</b> aux modules:<ul><li>"
% (etud["nomprenom"], etud["ne"])
)
H.append(
"</li><li>".join(
[
"%s (%s)"
% (
modsdict[x]["module"]["titre"],
modsdict[x]["module"]["code"] or "(module sans code)",
)
for x in a_inscrire
]
)
+ "</p>"
)
H.append("</li></ul>")
modulesimpls_ainscrire = ",".join(str(x) for x in a_inscrire)
modulesimpls_adesinscrire = ",".join(str(x) for x in a_desinscrire)
a_inscrire = set()
for ue in ues:
a_inscrire.update(
int(x) for x in tf[2][f"moduleimpls_{ue.id}"]
) # conversion en int !
# supprime ceux auquel deja inscrit:
for ins in inscr:
if ins["moduleimpl_id"] in a_inscrire:
a_inscrire.remove(ins["moduleimpl_id"])
# dict des modules:
modimpls_by_id = {modimpl.id: modimpl for modimpl in formsemestre.modimpls}
#
if (not a_inscrire) and (not a_desinscrire):
H.append(
"""<form action="do_moduleimpl_incription_options">
<input type="hidden" name="etudid" value="%s"/>
<input type="hidden" name="modulesimpls_ainscrire" value="%s"/>
<input type="hidden" name="modulesimpls_adesinscrire" value="%s"/>
<input type ="submit" value="Confirmer"/>
<input type ="button" value="Annuler" onclick="document.location='%s';"/>
</form>
f"""<h3>Aucune modification à effectuer</h3>
<p><a class="stdlink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">retour à la fiche étudiant</a></p>
"""
% (
etudid,
modulesimpls_ainscrire,
modulesimpls_adesinscrire,
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
)
)
return "\n".join(H) + footer
H.append("<h3>Confirmer les modifications:</h3>")
if a_desinscrire:
H.append(
f"""<p>{etud.nomprenom} va être <b>désinscrit{etud.e}</b> des modules:<ul><li>"""
)
H.append(
"</li><li>".join(
[
f"""{modimpls_by_id[x].module.titre or ''} ({
modimpls_by_id[x].module.code or '(module sans code)'})"""
for x in a_desinscrire
]
)
+ "</p>"
)
H.append("</li></ul>")
if a_inscrire:
H.append(
f"""<p>{etud.nomprenom} va être <b>inscrit{etud.e}</b> aux modules:<ul><li>"""
)
H.append(
"</li><li>".join(
[
f"""{modimpls_by_id[x].module.titre or ''} ({
modimpls_by_id[x].module.code or '(module sans code)'})"""
for x in a_inscrire
]
)
+ "</p>"
)
H.append("</li></ul>")
modulesimpls_ainscrire = ",".join(str(x) for x in a_inscrire)
modulesimpls_adesinscrire = ",".join(str(x) for x in a_desinscrire)
H.append(
f"""
<form action="do_moduleimpl_incription_options">
<input type="hidden" name="etudid" value="{etudid}"/>
<input type="hidden" name="modulesimpls_ainscrire" value="{modulesimpls_ainscrire}"/>
<input type="hidden" name="modulesimpls_adesinscrire" value="{modulesimpls_adesinscrire}"/>
<input type ="submit" value="Confirmer"/>
<input type ="button" value="Annuler" onclick="document.location='{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}';"/>
</form>
"""
)
return "\n".join(H) + footer
def do_moduleimpl_incription_options(
etudid, modulesimpls_ainscrire, modulesimpls_adesinscrire

View File

@ -909,37 +909,6 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
return "\n".join(H)
def html_expr_diagnostic(diagnostics):
"""Affiche messages d'erreur des formules utilisateurs"""
H = []
H.append('<div class="ue_warning">Erreur dans des formules utilisateurs:<ul>')
last_id, last_msg = None, None
for diag in diagnostics:
if "moduleimpl_id" in diag:
mod = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=diag["moduleimpl_id"]
)[0]
H.append(
'<li>module <a href="moduleimpl_status?moduleimpl_id=%s">%s</a>: %s</li>'
% (
diag["moduleimpl_id"],
mod["module"]["abbrev"] or mod["module"]["code"] or "?",
diag["msg"],
)
)
else:
if diag["ue_id"] != last_id or diag["msg"] != last_msg:
ue = sco_edit_ue.ue_list({"ue_id": diag["ue_id"]})[0]
H.append(
'<li>UE "%s": %s</li>'
% (ue["acronyme"] or ue["titre"] or "?", diag["msg"])
)
last_id, last_msg = diag["ue_id"], diag["msg"]
H.append("</ul></div>")
return "".join(H)
def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None):
"""En-tête HTML des pages "semestre" """
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
@ -1081,9 +1050,6 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
>Toutes évaluations (même incomplètes) visibles</div>"""
)
if nt.expr_diagnostics:
H.append(html_expr_diagnostic(nt.expr_diagnostics))
if nt.parcours.APC_SAE:
# BUT: tableau ressources puis SAE
ressources = [

View File

@ -1217,7 +1217,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
<div id="ue_list_code" class="sco_box sco_green_bg">
<!-- filled by ue_sharing_code -->
</div>
{check_formation_ues(formation.id)[0]}
{check_formation_ues(formation)[0]}
{html_sco_header.sco_footer()}
"""
@ -1376,15 +1376,14 @@ def _invalidate_etud_formation_caches(etudid, formation_id):
) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif)
def check_formation_ues(formation_id):
def check_formation_ues(formation: Formation) -> tuple[str, dict[int, list[UniteEns]]]:
"""Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id
Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de
définition du programme: cette fonction retourne un bout de HTML
à afficher pour prévenir l'utilisateur, ou '' si tout est ok.
"""
ues = sco_edit_ue.ue_list({"formation_id": formation_id})
ue_multiples = {} # { ue_id : [ liste des formsemestre ] }
for ue in ues:
for ue in formation.ues:
# formsemestres utilisant cette ue ?
sems = ndb.SimpleDictFetch(
"""SELECT DISTINCT sem.id AS formsemestre_id, sem.*
@ -1394,9 +1393,9 @@ def check_formation_ues(formation_id):
AND mi.formsemestre_id = sem.id
AND mod.ue_id = %(ue_id)s
""",
{"ue_id": ue["ue_id"], "formation_id": formation_id},
{"ue_id": ue.id, "formation_id": formation.id},
)
semestre_ids = set([x["semestre_id"] for x in sems])
semestre_ids = {x["semestre_id"] for x in sems}
if (
len(semestre_ids) > 1
): # plusieurs semestres d'indices differents dans le cursus
@ -1416,11 +1415,11 @@ def check_formation_ues(formation_id):
<ul>
"""
]
for ue in ues:
if ue["ue_id"] in ue_multiples:
for ue in formation.ues:
if ue.id in ue_multiples:
sems = [
sco_formsemestre.get_formsemestre(x["formsemestre_id"])
for x in ue_multiples[ue["ue_id"]]
for x in ue_multiples[ue.id]
]
slist = ", ".join(
[
@ -1429,7 +1428,7 @@ def check_formation_ues(formation_id):
for s in sems
]
)
H.append("<li><b>%s</b> : %s</li>" % (ue["acronyme"], slist))
H.append("<li><b>%s</b> : %s</li>" % (ue.acronyme, slist))
H.append("</ul></div>")
return "\n".join(H), ue_multiples

View File

@ -56,6 +56,7 @@ _moduleimplEditor = ndb.EditableTable(
def do_moduleimpl_create(args):
"create a moduleimpl"
# TODO remplacer par une methode de ModuleImpl qui appelle super().create_from_dict() puis invalide le formsemestre
cnx = ndb.GetDBConnexion()
r = _moduleimplEditor.create(cnx, args)
sco_cache.invalidate_formsemestre(
@ -109,91 +110,6 @@ def do_moduleimpl_edit(args, formsemestre_id=None, cnx=None):
) # > modif moduleimpl
def moduleimpl_withmodule_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, sort_by_ue=False
) -> list:
"""Liste les moduleimpls et ajoute dans chacun
l'UE, la matière et le module auxquels ils appartiennent.
Tri la liste par:
- pour les formations classiques: semestre/UE/numero_matiere/numero_module;
- pour le BUT: ignore UEs sauf si sort_by_ue et matières dans le tri.
NB: Cette fonction faisait partie de l'API ScoDoc 7.
"""
from app.scodoc import sco_edit_ue
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
modimpls = moduleimpl_list(
**{
"moduleimpl_id": moduleimpl_id,
"formsemestre_id": formsemestre_id,
"module_id": module_id,
}
)
if not modimpls:
return []
ues = {}
matieres = {}
modules = {}
for mi in modimpls:
module_id = mi["module_id"]
if not mi["module_id"] in modules:
modules[module_id] = sco_edit_module.module_list(
args={"module_id": module_id}
)[0]
mi["module"] = modules[module_id]
ue_id = mi["module"]["ue_id"]
if not ue_id in ues:
ues[ue_id] = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
mi["ue"] = ues[ue_id]
matiere_id = mi["module"]["matiere_id"]
if not matiere_id in matieres:
matieres[matiere_id] = sco_edit_matiere.matiere_list(
args={"matiere_id": matiere_id}
)[0]
mi["matiere"] = matieres[matiere_id]
mod = modimpls[0]["module"]
formation = db.session.get(Formation, mod["formation_id"])
if formation.is_apc():
# tri par numero_module
if sort_by_ue:
modimpls.sort(
key=lambda x: (
x["ue"]["numero"],
x["ue"]["ue_id"],
x["module"]["module_type"],
x["module"]["numero"],
x["module"]["code"],
)
)
else:
modimpls.sort(
key=lambda x: (
x["module"]["module_type"],
x["module"]["numero"],
x["module"]["code"],
)
)
else:
# Formations classiques, avec matières:
# tri par semestre/UE/numero_matiere/numero_module
modimpls.sort(
key=lambda x: (
x["ue"]["numero"],
x["ue"]["ue_id"],
x["matiere"]["numero"],
x["matiere"]["matiere_id"],
x["module"]["numero"],
x["module"]["code"],
)
)
return modimpls
def moduleimpls_in_external_ue(ue_id):
"""List of modimpls in this ue"""
cursor = ndb.SimpleQuery(
@ -254,9 +170,9 @@ _moduleimpl_inscriptionEditor = ndb.EditableTable(
)
def do_moduleimpl_inscription_create(args, formsemestre_id=None):
def do_moduleimpl_inscription_create(args, formsemestre_id=None, cnx=None):
"create a moduleimpl_inscription"
cnx = ndb.GetDBConnexion()
cnx = cnx or ndb.GetDBConnexion()
try:
r = _moduleimpl_inscriptionEditor.create(cnx, args)
except psycopg2.errors.UniqueViolation as exc:
@ -270,7 +186,7 @@ def do_moduleimpl_inscription_create(args, formsemestre_id=None):
cnx,
method="moduleimpl_inscription",
etudid=args["etudid"],
msg="inscription module %s" % args["moduleimpl_id"],
msg=f"inscription module {args['moduleimpl_id']}",
commit=False,
)
return r
@ -297,32 +213,29 @@ def do_moduleimpl_inscrit_etuds(moduleimpl_id, formsemestre_id, etudids, reset=F
args={"formsemestre_id": formsemestre_id, "etudid": etudid}
)
if not insem:
raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)
raise ScoValueError(f"{etudid} n'est pas inscrit au semestre !")
cnx = ndb.GetDBConnexion()
# Desinscriptions
if reset:
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"delete from notes_moduleimpl_inscription where moduleimpl_id = %(moduleimpl_id)s",
{"moduleimpl_id": moduleimpl_id},
)
# Inscriptions au module:
inmod_set = set(
[
# hum ?
x["etudid"]
for x in do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
]
)
inmod_set = {
x["etudid"] for x in do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
}
for etudid in etudids:
# deja inscrit ?
# déja inscrit ?
if not etudid in inmod_set:
do_moduleimpl_inscription_create(
{"moduleimpl_id": moduleimpl_id, "etudid": etudid},
formsemestre_id=formsemestre_id,
cnx=cnx,
)
cnx.commit()
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > moduleimpl_inscrit_etuds

View File

@ -409,34 +409,32 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
H.append(
'<h3>Étudiants avec UEs capitalisées (ADM):</h3><ul class="ue_inscr_list">'
)
ues = [
sco_edit_ue.ue_list({"ue_id": ue_id})[0] for ue_id in ues_cap_info.keys()
]
ues.sort(key=lambda u: u["numero"])
ues = [UniteEns.query.get_or_404(ue_id) for ue_id in ues_cap_info.keys()]
ues.sort(key=lambda u: u.numero)
for ue in ues:
H.append(
f"""<li class="tit"><span class="tit">{ue['acronyme']}: {ue['titre']}</span>"""
f"""<li class="tit"><span class="tit">{ue.acronyme}: {ue.titre or ''}</span>"""
)
H.append("<ul>")
for info in ues_cap_info[ue["ue_id"]]:
etud = sco_etud.get_etud_info(etudid=info["etudid"], filled=True)[0]
for info in ues_cap_info[ue.id]:
etud = Identite.get_etud(info["etudid"])
H.append(
f"""<li class="etud"><a class="discretelink etudinfo"
id="{info['etudid']}"
id="{etud.id}"
href="{
url_for(
"scolar.fiche_etud",
scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"],
etudid=etud.id,
)
}">{etud["nomprenom"]}</a>"""
}">{etud.nomprenom}</a>"""
)
if info["ue_status"]["event_date"]:
H.append(
f"""(cap. le {info["ue_status"]["event_date"].strftime("%d/%m/%Y")})"""
)
if is_apc:
is_inscrit_ue = (etud["etudid"], ue["id"]) not in res.dispense_ues
is_inscrit_ue = (etud.id, ue.id) not in res.dispense_ues
else:
# CLASSIQUE
is_inscrit_ue = info["is_ins"]
@ -468,8 +466,8 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
H.append(
f"""<div><a class="stdlink" href="{
url_for("notes.etud_desinscrit_ue",
scodoc_dept=g.scodoc_dept, etudid=etud["etudid"],
formsemestre_id=formsemestre_id, ue_id=ue["ue_id"])
scodoc_dept=g.scodoc_dept, etudid=etud.id,
formsemestre_id=formsemestre_id, ue_id=ue.id)
}">désinscrire {"des modules" if not is_apc else ""} de cette UE</a></div>
"""
)
@ -479,8 +477,8 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
H.append(
f"""<div><a class="stdlink" href="{
url_for("notes.etud_inscrit_ue",
scodoc_dept=g.scodoc_dept, etudid=etud["etudid"],
formsemestre_id=formsemestre_id, ue_id=ue["ue_id"])
scodoc_dept=g.scodoc_dept, etudid=etud.id,
formsemestre_id=formsemestre_id, ue_id=ue.id)
}">inscrire à {"" if is_apc else "tous les modules de"} cette UE</a></div>
"""
)

View File

@ -127,12 +127,12 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
"args": {
"group_ids": group_id,
"evaluation_id": evaluation.id,
"date_debut": evaluation.date_debut.isoformat()
if evaluation.date_debut
else "",
"date_fin": evaluation.date_fin.isoformat()
if evaluation.date_fin
else "",
"date_debut": (
evaluation.date_debut.isoformat() if evaluation.date_debut else ""
),
"date_fin": (
evaluation.date_fin.isoformat() if evaluation.date_fin else ""
),
},
"enabled": evaluation.date_debut is not None
and evaluation.date_fin is not None,
@ -355,10 +355,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.
</div>"""
)
#
if has_expression and nt.expr_diagnostics:
H.append(sco_formsemestre_status.html_expr_diagnostic(nt.expr_diagnostics))
#
if formsemestre_has_decisions(formsemestre_id):
H.append(
"""<ul class="tf-msg">

View File

@ -139,9 +139,8 @@ def dict_pvjury(
dec_ue_list = _descr_decisions_ues(
nt, etudid, d["decisions_ue"], d["decision_sem"]
)
d["decisions_ue_nb"] = len(
dec_ue_list
) # avec les UE capitalisées, donc des éventuels doublons
# avec les UE capitalisées, donc des éventuels doublons:
d["decisions_ue_nb"] = len(dec_ue_list)
# Mais sur la description (eg sur les bulletins), on ne veut pas
# afficher ces doublons: on uniquifie sur ue_code
_codes = set()
@ -291,8 +290,10 @@ def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]:
)
)
):
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
uelist.append(ue)
ue = UniteEns.query.get(ue_id)
assert ue
# note modernisation code: on utilise des dict tant que get_etud_ue_status renvoie des dicts
uelist.append(ue.to_dict())
# Les UE capitalisées dans d'autres semestres:
if etudid in nt.validations.ue_capitalisees.index:
for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]:

View File

@ -528,6 +528,7 @@ def notes_add(
Return: tuple (etudids_changed, nb_suppress, etudids_with_decision)
"""
assert evaluation_id is not None
now = psycopg2.Timestamp(*time.localtime()[:6])
# Vérifie inscription et valeur note
@ -539,7 +540,7 @@ def notes_add(
}
for etudid, value in notes:
if check_inscription and (etudid not in inscrits):
raise NoteProcessError(f"etudiant {etudid} non inscrit dans ce module")
raise NoteProcessError(f"étudiant {etudid} non inscrit dans ce module")
if (value is not None) and not isinstance(value, float):
raise NoteProcessError(
f"etudiant {etudid}: valeur de note invalide ({value})"

View File

@ -60,16 +60,14 @@ from app.models.formsemestre import FormSemestre
from app import db, log
from app.models import Evaluation, ModuleImpl, UniteEns
from app.models import Evaluation, Identite, ModuleImpl, UniteEns
from app.scodoc import html_sco_header
from app.scodoc import codes_cursus
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_saisie_notes
from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
@ -83,10 +81,10 @@ def external_ue_create(
acronyme="",
ue_type=codes_cursus.UE_STANDARD,
ects=0.0,
) -> int:
) -> ModuleImpl:
"""Crée UE/matiere/module dans la formation du formsemestre
puis un moduleimpl.
Return: moduleimpl_id
Return: moduleimpl
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
log(f"creating external UE in {formsemestre}: {acronyme}")
@ -139,28 +137,30 @@ def external_ue_create(
"module_id": module_id,
"formsemestre_id": formsemestre_id,
# affecte le 1er responsable du semestre comme resp. du module
"responsable_id": formsemestre.responsables[0].id
if len(formsemestre.responsables)
else None,
"responsable_id": (
formsemestre.responsables[0].id
if len(formsemestre.responsables)
else None
),
},
)
return moduleimpl_id
modimpl = ModuleImpl.query.get(moduleimpl_id)
assert modimpl
return modimpl
def external_ue_inscrit_et_note(
moduleimpl_id: int, formsemestre_id: int, notes_etuds: dict
moduleimpl: ModuleImpl, formsemestre_id: int, notes_etuds: dict
):
"""Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
et enregistre les notes.
"""
moduleimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
log(
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl.id}, notes_etuds={notes_etuds})"
)
# Inscription des étudiants
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
moduleimpl_id,
moduleimpl.id,
formsemestre_id,
list(notes_etuds.keys()),
)
@ -188,12 +188,12 @@ def external_ue_inscrit_et_note(
)
def get_existing_external_ue(formation_id: int) -> list[dict]:
"Liste de toutes les UE externes définies dans cette formation"
return sco_edit_ue.ue_list(args={"formation_id": formation_id, "is_external": True})
def get_existing_external_ue(formation_id: int) -> list[UniteEns]:
"Liste de toutes les UEs externes définies dans cette formation"
return UniteEns.query.filter_by(formation_id=formation_id, is_external=True).all()
def get_external_moduleimpl_id(formsemestre_id: int, ue_id: int) -> int:
def get_external_moduleimpl(formsemestre_id: int, ue_id: int) -> ModuleImpl:
"moduleimpl correspondant à l'UE externe indiquée de ce formsemestre"
r = ndb.SimpleDictFetch(
"""
@ -205,7 +205,10 @@ def get_external_moduleimpl_id(formsemestre_id: int, ue_id: int) -> int:
{"ue_id": ue_id, "formsemestre_id": formsemestre_id},
)
if r:
return r[0]["moduleimpl_id"]
modimpl_id = r[0]["moduleimpl_id"]
modimpl = ModuleImpl.query.get(modimpl_id)
assert modimpl
return modimpl
else:
raise ScoValueError(
f"""Aucun module externe ne correspond
@ -225,20 +228,20 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
En BUT, pas d'UEs externes. Voir https://scodoc.org/git/ScoDoc/ScoDoc/issues/542
"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Contrôle d'accès:
if not formsemestre.can_be_edited_by(current_user):
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
if formsemestre.formation.is_apc():
raise ScoValueError("Impossible d'ajouter une UE externe en BUT")
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etud = Identite.get_etud(etudid)
formation_id = formsemestre.formation.id
existing_external_ue = get_existing_external_ue(formation_id)
H = [
html_sco_header.html_sem_header(
"Ajout d'une UE externe pour %(nomprenom)s" % etud,
f"Ajout d'une UE externe pour {etud.nomprenom}",
javascripts=["js/sco_ue_external.js"],
),
"""<p class="help">Cette page permet d'indiquer que l'étudiant a suivi une UE
@ -275,10 +278,10 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
"input_type": "menu",
"title": "UE externe existante:",
"allowed_values": [""]
+ [str(ue["ue_id"]) for ue in existing_external_ue],
+ [str(ue.id) for ue in existing_external_ue],
"labels": [default_label]
+ [
"%s (%s)" % (ue["titre"], ue["acronyme"])
f"{ue.titre or ''} ({ue.acronyme})"
for ue in existing_external_ue
],
"attributes": ['onchange="update_external_ue_form();"'],
@ -364,7 +367,7 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
)
if tf[2]["existing_ue"]:
ue_id = int(tf[2]["existing_ue"])
moduleimpl_id = get_external_moduleimpl_id(formsemestre_id, ue_id)
modimpl = get_external_moduleimpl(formsemestre_id, ue_id)
else:
acronyme = tf[2]["acronyme"].strip()
if not acronyme:
@ -375,7 +378,7 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
+ tf[1]
+ html_footer
)
moduleimpl_id = external_ue_create(
modimpl = external_ue_create(
formsemestre_id,
titre=tf[2]["titre"],
acronyme=acronyme,
@ -384,7 +387,7 @@ def external_ue_create_form(formsemestre_id: int, etudid: int):
)
external_ue_inscrit_et_note(
moduleimpl_id,
modimpl,
formsemestre_id,
{etudid: note_value},
)

View File

@ -23,7 +23,7 @@
<h2 class="insidebar">Scolarité</h2>
<a href="{{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Semestres</a> <br>
<a href="{{url_for('notes.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Programmes</a> <br>
<a href="{{url_for('notes.index_html', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Formations</a> <br>
{% if current_user.has_permission(sco.Permission.AbsChange)%}
<a href="{{url_for('assiduites.bilan_dept', scodoc_dept=g.scodoc_dept)}}" class="sidebar">Assiduité</a> <br>

View File

@ -30,6 +30,7 @@ Module notes: issu de ScoDoc7 / ZNotes.py
Emmanuel Viennet, 2021
"""
import html
from operator import itemgetter
import time
@ -487,7 +488,6 @@ def get_ue_niveaux_options_html():
return apc_edit_ue.get_ue_niveaux_options_html(ue)
@bp.route("/ue_list") # backward compat
@bp.route("/ue_table")
@scodoc
@permission_required(Permission.ScoView)
@ -682,21 +682,21 @@ def module_clone():
@bp.route("/index_html")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def index_html():
"Page accueil formations"
fmt = request.args.get("fmt", "html")
editable = current_user.has_permission(Permission.EditFormation)
table = sco_formations.formation_list_table()
if fmt != "html":
return table.make_page(fmt=fmt, filename=f"Formations-{g.scodoc_dept}")
H = [
html_sco_header.sco_header(page_title="Programmes formations"),
"""<h2>Programmes pédagogiques</h2>
""",
html_sco_header.sco_header(page_title="Formations (programmes)"),
"""<h2>Formations (programmes pédagogiques)</h2>
""",
table.html(),
]
T = sco_formations.formation_list_table()
H.append(T.html())
if editable:
H.append(
f"""
@ -804,7 +804,7 @@ def formation_import_xml_form():
<h2>Import effectué !</h2>
<ul>
<li><a class="stdlink" href="{
url_for("notes.ue_list", scodoc_dept=g.scodoc_dept, formation_id=formation_id
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id
)}">Voir la formation</a>
</li>
<li><a class="stdlink" href="{
@ -817,19 +817,6 @@ def formation_import_xml_form():
"""
# sco_publish(
# "/formation_create_new_version",
# sco_formations.formation_create_new_version,
# Permission.EditFormation,
# )
# --- UE
sco_publish(
"/ue_list",
sco_edit_ue.ue_list,
Permission.ScoView,
)
sco_publish("/module_move", sco_edit_formation.module_move, Permission.EditFormation)
sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.EditFormation)
@ -3284,11 +3271,12 @@ def check_sem_integrity(formsemestre_id, fix=False):
for modimpl in modimpls:
mod = sco_edit_module.module_list({"module_id": modimpl["module_id"]})[0]
formations_set.add(mod["formation_id"])
ue = sco_edit_ue.ue_list({"ue_id": mod["ue_id"]})[0]
formations_set.add(ue["formation_id"])
if ue["formation_id"] != mod["formation_id"]:
ue = UniteEns.query.get_or_404(mod["ue_id"])
ue_dict = ue.to_dict()
formations_set.add(ue_dict["formation_id"])
if ue_dict["formation_id"] != mod["formation_id"]:
modimpl["mod"] = mod
modimpl["ue"] = ue
modimpl["ue"] = ue_dict
bad_ue.append(modimpl)
if sem["formation_id"] != mod["formation_id"]:
bad_sem.append(modimpl)
@ -3341,30 +3329,28 @@ def check_sem_integrity(formsemestre_id, fix=False):
@permission_required(Permission.ScoView)
@scodoc7func
def check_form_integrity(formation_id, fix=False):
"debug"
log("check_form_integrity: formation_id=%s fix=%s" % (formation_id, fix))
ues = sco_edit_ue.ue_list(args={"formation_id": formation_id})
"debug (obsolete)"
log(f"check_form_integrity: formation_id={formation_id} fix={fix}")
formation: Formation = Formation.query.filter_by(
dept_id=g.scodoc_dept_id, formation_id=formation_id
).first_or_404()
bad = []
for ue in ues:
mats = sco_edit_matiere.matiere_list(args={"ue_id": ue["ue_id"]})
for mat in mats:
mods = sco_edit_module.module_list({"matiere_id": mat["matiere_id"]})
for mod in mods:
if mod["ue_id"] != ue["ue_id"]:
for ue in formation.ues:
for matiere in ue.matieres:
for mod in matiere.modules:
if mod.ue_id != ue.id:
if fix:
# fix mod.ue_id
log(
"fix: mod.ue_id = %s (was %s)" % (ue["ue_id"], mod["ue_id"])
)
mod["ue_id"] = ue["ue_id"]
sco_edit_module.do_module_edit(mod)
log(f"fix: mod.ue_id = {ue.id} (was {mod.ue_id})")
mod.ue_id = ue.id
db.session.add(mod)
bad.append(mod)
if mod["formation_id"] != formation_id:
if mod.formation_id != formation_id:
bad.append(mod)
if bad:
txth = "<br>".join([str(x) for x in bad])
txth = "<br>".join([html.escape(str(x)) for x in bad])
txt = "\n".join([str(x) for x in bad])
log("check_form_integrity: formation_id=%s\ninconsistencies:" % formation_id)
log(f"check_form_integrity: formation_id={formation_id}\ninconsistencies:")
log(txt)
# Notify by e-mail
send_scodoc_alarm("Notes: formation incoherente !", txt)
@ -3380,39 +3366,31 @@ def check_form_integrity(formation_id, fix=False):
@scodoc7func
def check_formsemestre_integrity(formsemestre_id):
"debug"
log("check_formsemestre_integrity: formsemestre_id=%s" % (formsemestre_id))
log(f"check_formsemestre_integrity: formsemestre_id={formsemestre_id}")
# verifie que tous les moduleimpl d'un formsemestre
# se réfèrent à un module dont l'UE appartient a la même formation
# Ancien bug: les ue_id étaient mal copiés lors des création de versions
# de formations
diag = []
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(formsemestre_id=formsemestre_id)
for mod in Mlist:
if mod["module"]["ue_id"] != mod["matiere"]["ue_id"]:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
for modimpl in formsemestre.modimpls:
if modimpl.module.ue_id != modimpl.module.matiere.ue_id:
diag.append(
"moduleimpl %s: module.ue_id=%s != matiere.ue_id=%s"
% (
mod["moduleimpl_id"],
mod["module"]["ue_id"],
mod["matiere"]["ue_id"],
)
f"""moduleimpl {modimpl.id}: module.ue_id={modimpl.module.ue_id
} != matiere.ue_id={modimpl.module.matiere.ue_id}"""
)
if mod["ue"]["formation_id"] != mod["module"]["formation_id"]:
if modimpl.module.ue.formation_id != modimpl.module.formation_id:
diag.append(
"moduleimpl %s: ue.formation_id=%s != mod.formation_id=%s"
% (
mod["moduleimpl_id"],
mod["ue"]["formation_id"],
mod["module"]["formation_id"],
)
f"""moduleimpl {modimpl.id}: ue.formation_id={
modimpl.module.ue.formation_id} != mod.formation_id={
modimpl.module.formation_id}"""
)
if diag:
send_scodoc_alarm(
"Notes: formation incoherente dans semestre %s !" % formsemestre_id,
f"Notes: formation incoherente dans semestre {formsemestre_id} !",
"\n".join(diag),
)
log("check_formsemestre_integrity: formsemestre_id=%s" % formsemestre_id)
log(f"check_formsemestre_integrity: formsemestre_id={formsemestre_id}")
log("inconsistencies:\n" + "\n".join(diag))
else:
diag = ["OK"]

View File

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

View File

@ -249,37 +249,7 @@ def test_formations(test_client):
assert len(lim_modid) == 1
lim_modimpl_id = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
# print(lim_modimpl_id)
# ---- Test de moduleimpl_withmodule_list
assert lim_modid == lim_modimpl_id # doit etre le meme resultat
liimp_sem1 = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=sem1["formsemestre_id"]
)
assert len(liimp_sem1) == 2
assert module_id in (
liimp_sem1[0]["module_id"],
liimp_sem1[1]["module_id"],
)
assert module_id2 in (
liimp_sem1[0]["module_id"],
liimp_sem1[1]["module_id"],
)
liimp_sem2 = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=sem2["formsemestre_id"]
)
assert module_id_t == liimp_sem2[0]["module_id"]
liimp_modid = sco_moduleimpl.moduleimpl_withmodule_list(module_id=module_id)
assert len(liimp_modid) == 1
liimp_modimplid = sco_moduleimpl.moduleimpl_withmodule_list(
moduleimpl_id=moduleimpl_id
)
assert liimp_modid == liimp_modimplid
assert lim_modid == lim_modimpl_id
# --- Suppression du module, matiere et ue test du semestre 2

View File

@ -54,7 +54,7 @@ RELEASE=1
ARCH="amd64"
FACTORY_DIR="/opt/factory"
DEST_DIR="$PACKAGE_NAME"_"$VERSION"-"$RELEASE"_"$ARCH"
GIT_RELEASE_URL="https://scodoc.org/git/viennet/ScoDoc/archive/${RELEASE_TAG}.tar.gz"
GIT_RELEASE_URL="https://scodoc.org/git/ScoDoc/ScoDoc/archive/${RELEASE_TAG}.tar.gz"
UNIT_TESTS_DIR="/opt/scodoc" # on lance les tests dans le rep. de travail, pas idéal
echo "Le paquet sera $DEST_DIR.deb"