Compare commits

...

10 Commits

38 changed files with 517 additions and 284 deletions

View File

@ -337,7 +337,7 @@ def assiduites_group(with_query: bool = False):
try:
etuds = [int(etu) for etu in etuds]
except ValueError:
return json_error(404, "Le champs etudids n'est pas correctement formé")
return json_error(404, "Le champ etudids n'est pas correctement formé")
# Vérification que tous les étudiants sont du même département
query = Identite.query.filter(Identite.id.in_(etuds))

View File

@ -447,8 +447,24 @@ def formsemestre_warning_apc_setup(
}">formation n'est pas associée à un référentiel de compétence.</a>
</div>
"""
# Vérifie les niveaux de chaque parcours
H = []
# Le semestre n'a pas de parcours, mais les UE ont des parcours ?
if not formsemestre.parcours:
nb_ues_sans_parcours = len(
formsemestre.formation.query_ues_parcour(None)
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
.all()
)
nb_ues_tot = (
UniteEns.query.filter_by(formation=formsemestre.formation, type=UE_STANDARD)
.filter(UniteEns.semestre_idx == formsemestre.semestre_id)
.count()
)
if nb_ues_sans_parcours != nb_ues_tot:
H.append(
f"""Le semestre n'est associé à aucun parcours, mais les UEs de la formation ont des parcours"""
)
# Vérifie les niveaux de chaque parcours
for parcour in formsemestre.parcours or [None]:
annee = (formsemestre.semestre_id + 1) // 2
niveaux_ids = {

View File

@ -256,7 +256,7 @@ def _gen_but_niveau_ue(
return f"""<div class="but_niveau_ue {ue_class}
{'annee_prec' if annee_prec else ''}
">
<div title="{ue.titre}">{ue.acronyme}</div>
<div title="{ue.titre or ''}">{ue.acronyme}</div>
<div class="but_note with_scoplement">
<div>{moy_ue_str}</div>
{scoplement}

View File

@ -17,7 +17,7 @@ def UEParcoursECTSForm(ue: UniteEns) -> FlaskForm:
pass
parcours: list[ApcParcours] = ue.formation.referentiel_competence.parcours
# Initialise un champs de saisie par parcours
# Initialise un champ de saisie par parcours
for parcour in parcours:
ects = ue.get_ects(parcour, only_parcours=True)
setattr(

View File

@ -82,7 +82,7 @@ class ConfigCASForm(FlaskForm):
cas_attribute_id = StringField(
label="Attribut CAS utilisé comme id (laissez vide pour prendre l'id par défaut)",
description="""Le champs CAS qui sera considéré comme l'id unique des
description="""Le champ CAS qui sera considéré comme l'id unique des
comptes utilisateurs.""",
)

View File

@ -297,7 +297,7 @@ class ScoDocSiteConfig(db.Model):
@classmethod
def _get_int_field(cls, name: str, default=None) -> int:
"""Valeur d'un champs integer"""
"""Valeur d'un champ integer"""
cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
if (cfg is None) or cfg.value is None:
return default
@ -311,7 +311,7 @@ class ScoDocSiteConfig(db.Model):
default=None,
range_values: tuple = (),
) -> bool:
"""Set champs integer. True si changement."""
"""Set champ integer. True si changement."""
if value != cls._get_int_field(name, default=default):
if not isinstance(value, int) or (
range_values and (value < range_values[0]) or (value > range_values[1])

View File

@ -6,18 +6,38 @@
"""Affichages, debug
"""
from flask import g
from app import log
PE_DEBUG = 0
PE_DEBUG = True
if not PE_DEBUG:
# log to notes.log
def pe_print(*a, **kw):
# kw is ignored. log always add a newline
log(" ".join(a))
else:
pe_print = print # print function
# On stocke les logs PE dans g.scodoc_pe_log
# pour ne pas modifier les nombreux appels à pe_print.
def pe_start_log() -> list[str]:
"Initialize log"
g.scodoc_pe_log = []
return g.scodoc_pe_log
def pe_print(*a):
"Log (or print in PE_DEBUG mode) and store in g"
if PE_DEBUG:
msg = " ".join(a)
print(msg)
else:
lines = getattr(g, "scodoc_pe_log")
if lines is None:
lines = pe_start_log()
msg = " ".join(a)
lines.append(msg)
log(msg)
def pe_get_log() -> str:
"Renvoie une chaîne avec tous les messages loggués"
return "\n".join(getattr(g, "scodoc_pe_log", []))
# Affichage dans le tableur pe en cas d'absence de notes
SANS_NOTE = "-"

View File

@ -284,3 +284,13 @@ def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]:
cosemestres[fid] = cosem
return cosemestres
def tri_semestres_par_rang(cosemestres: dict[int, FormSemestre]):
"""Partant d'un dictionnaire de cosemestres, les tri par rang (semestre_id) dans un
dictionnaire {rang: [liste des semestres du dit rang]}"""
cosemestres_tries = {}
for sem in cosemestres.values():
cosemestres_tries[sem.semestre_id] = cosemestres_tries.get(sem.semestre_id, []) + [sem]
return cosemestres_tries

View File

@ -37,6 +37,7 @@ Created on 17/01/2024
"""
import pandas as pd
from app import ScoValueError
from app.models import FormSemestre, Identite, Formation
from app.pe import pe_comp, pe_affichage
from app.scodoc import codes_cursus
@ -141,7 +142,7 @@ class EtudiantsJuryPE:
assert nbre_abandons == len(self.abandons_ids)
pe_affichage.pe_print(
f" => {nbre_abandons} étudiants non considérés (redoublement, réorientation, abandon"
f" => {nbre_abandons} étudiants traités mais non diplômés (redoublement, réorientation, abandon)"
)
# pe_affichage.pe_print(
# " => quelques étudiants futurs diplômés : "
@ -187,7 +188,11 @@ class EtudiantsJuryPE:
etudiants = {etudid: self.identites[etudid] for etudid in etudids}
return etudiants
def analyse_etat_etudiant(self, etudid: int, cosemestres: dict[int, FormSemestre]):
def analyse_etat_etudiant(
self,
etudid: int,
cosemestres: dict[int, FormSemestre]
):
"""Analyse le cursus d'un étudiant pouvant être :
* l'un de ceux sur lesquels le jury va statuer (année de diplômation du jury considéré)
@ -198,8 +203,11 @@ class EtudiantsJuryPE:
* à insérer une entrée dans ``self.cursus`` pour mémoriser son identité,
avec son nom, prénom, etc...
* à analyser son parcours, pour déterminer s'il n'a (ou non) abandonné l'IUT en cours de
route (cf. clé abandon)
* à analyser son parcours, pour déterminer s'il a démissionné, redoublé (autre année de diplôme)
ou a abandonné l'IUT en cours de route (cf. clé abandon). Un étudiant est considéré
en abandon si connaissant son dernier semestre (par ex. un S3) il n'est pas systématiquement
inscrit à l'un des S4, S5 ou S6 existants dans les cosemestres.
Args:
etudid: L'etudid d'un étudiant, à ajouter à ceux traiter par le jury
@ -232,15 +240,19 @@ class EtudiantsJuryPE:
"abandon": False, # va être traité en dessous
}
# Est-il démissionnaire : charge son dernier semestre pour connaitre son état ?
dernier_semes_etudiant = formsemestres[0]
res = load_formsemestre_results(dernier_semes_etudiant)
etud_etat = res.get_etud_etat(etudid)
if etud_etat == scu.DEMISSION:
self.cursus[etudid]["abandon"] |= True
else:
# Est-il réorienté ou a-t-il arrêté volontairement sa formation ?
self.cursus[etudid]["abandon"] |= arret_de_formation(identite, cosemestres)
# Si l'étudiant est succeptible d'être diplomé
if self.cursus[etudid]["diplome"] == self.annee_diplome:
# Est-il démissionnaire : charge son dernier semestre pour connaitre son état ?
dernier_semes_etudiant = formsemestres[0]
res = load_formsemestre_results(dernier_semes_etudiant)
etud_etat = res.get_etud_etat(etudid)
if etud_etat == scu.DEMISSION:
self.cursus[etudid]["abandon"] = True
else:
# Est-il réorienté ou a-t-il arrêté (volontairement) sa formation ?
self.cursus[etudid]["abandon"] = arret_de_formation(
identite, cosemestres
)
def get_semestres_significatifs(self, etudid: int):
"""Ensemble des semestres d'un étudiant, qui l'auraient amené à être diplomé
@ -446,8 +458,8 @@ def get_semestres_apc(identite: Identite) -> list:
return semestres_apc
def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
"""Détermine si un étudiant a arrêté sa formation. Il peut s'agir :
def arret_de_formation(etud: Identite, cosemestres: dict[int, FormSemestre]) -> bool:
"""Détermine si un étudiant a arrêté sa formation (volontairement ou non). Il peut s'agir :
* d'une réorientation à l'initiative du jury de semestre ou d'une démission
(on pourrait utiliser les code NAR pour réorienté & DEM pour démissionnaire
@ -458,7 +470,8 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas
dans l'un des "derniers" cosemestres (semestres conduisant à la même année de diplômation)
connu dans Scodoc.
connu dans Scodoc. Par "derniers" cosemestres, est fait le choix d'analyser tous les cosemestres
de rang/semestre_id supérieur (et donc de dates) au dernier semestre dans lequel il a été inscrit.
Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc),
l'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans
@ -485,50 +498,95 @@ def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
Est-il réorienté, démissionnaire ou a-t-il arrêté de son propre chef sa formation ?
TODO:: A reprendre pour le cas des étudiants à l'étranger
TODO:: A reprendre si BUT avec semestres décalés
"""
# Les semestres APC de l'étudiant
semestres = get_semestres_apc(etud)
semestres_apc = {sem.semestre_id: sem for sem in semestres}
if not semestres_apc:
return True
# Son dernier semestre APC en date
dernier_formsemestre = get_dernier_semestre_en_date(semestres_apc)
numero_dernier_formsemestre = dernier_formsemestre.semestre_id
# Le dernier semestre de l'étudiant
dernier_formsemestre = semestres[0]
rang_dernier_semestre = dernier_formsemestre.semestre_id
# Les numéro de semestres possible dans lesquels il pourrait s'incrire
# semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation)
if numero_dernier_formsemestre % 2 == 1:
numeros_possibles = list(
range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT)
)
# semestre pair => passage en année supérieure ou redoublement
else: #
numeros_possibles = list(
range(
max(numero_dernier_formsemestre - 1, 1),
pe_comp.NBRE_SEMESTRES_DIPLOMANT,
)
# Les cosemestres de rang supérieur ou égal à celui de formsemestre, triés par rang,
# sous la forme ``{semestre_id: [liste des comestres associé à ce semestre_id]}``
cosemestres_tries_par_rang = pe_comp.tri_semestres_par_rang(cosemestres)
cosemestres_superieurs = {}
for rang in cosemestres_tries_par_rang:
if rang > rang_dernier_semestre:
cosemestres_superieurs[rang] = cosemestres_tries_par_rang[rang]
# Si pas d'autres cosemestres postérieurs
if not cosemestres_superieurs:
return False
# Pour chaque rang de (co)semestres, y-a-il un dans lequel il est inscrit ?
etat_inscriptions = {rang: False for rang in cosemestres_superieurs}
for rang in etat_inscriptions:
for sem in cosemestres_superieurs[rang]:
etudiants_du_sem = {ins.etudid for ins in sem.inscriptions}
if etud.etudid in etudiants_du_sem:
etat_inscriptions[rang] = True
# Vérifie qu'il n'y a pas de "trous" dans les rangs des cosemestres
rangs = sorted(etat_inscriptions.keys())
if list(rangs) != list(range(min(rangs), max(rangs) + 1)):
difference = set(range(min(rangs), max(rangs) + 1)) - set(rangs)
affichage = ",".join([f"S{val}" for val in difference])
raise ScoValueError(
f"Il manque le(s) semestre(s) {affichage} au cursus de {etud.etat_civil} ({etud.etudid})."
)
# Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ?
formsestres_superieurs_possibles = []
for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits
if (
fid != dernier_formsemestre.formsemestre_id
and sem.semestre_id in numeros_possibles
and sem.date_debut.year >= dernier_formsemestre.date_debut.year
):
# date de debut des semestres possibles postérieur au dernier semestre de l'étudiant
# et de niveau plus élevé que le dernier semestre valide de l'étudiant
formsestres_superieurs_possibles.append(fid)
# Est-il inscrit à tous les semestres de rang supérieur ? Si non, est démissionnaire
est_demissionnaire = sum(etat_inscriptions.values()) != len(rangs)
if est_demissionnaire:
non_inscrit_a = [
rang for rang in etat_inscriptions if not etat_inscriptions[rang]
]
affichage = ",".join([f"S{val}" for val in non_inscrit_a])
pe_affichage.pe_print(
f"{etud.etat_civil} ({etud.etudid}) considéré en abandon car non inscrit dans un (ou des) semestre(s) {affichage} amenant à diplômation"
)
if len(formsestres_superieurs_possibles) > 0:
return True
return est_demissionnaire
return False
# # Son dernier semestre APC en date
# dernier_formsemestre = get_dernier_semestre_en_date(semestres_apc)
# numero_dernier_formsemestre = dernier_formsemestre.semestre_id
#
# # Les numéro de semestres possible dans lesquels il pourrait s'incrire
# # semestre impair => passage de droit en semestre pair suivant (effet de l'annualisation)
# if numero_dernier_formsemestre % 2 == 1:
# numeros_possibles = list(
# range(numero_dernier_formsemestre + 1, pe_comp.NBRE_SEMESTRES_DIPLOMANT)
# )
# # semestre pair => passage en année supérieure ou redoublement
# else: #
# numeros_possibles = list(
# range(
# max(numero_dernier_formsemestre - 1, 1),
# pe_comp.NBRE_SEMESTRES_DIPLOMANT,
# )
# )
#
# # Y-a-t-il des cosemestres dans lesquels il aurait pu s'incrire ?
# formsestres_superieurs_possibles = []
# for fid, sem in cosemestres.items(): # Les semestres ayant des inscrits
# if (
# fid != dernier_formsemestre.formsemestre_id
# and sem.semestre_id in numeros_possibles
# and sem.date_debut.year >= dernier_formsemestre.date_debut.year
# ):
# # date de debut des semestres possibles postérieur au dernier semestre de l'étudiant
# # et de niveau plus élevé que le dernier semestre valide de l'étudiant
# formsestres_superieurs_possibles.append(fid)
#
# if len(formsestres_superieurs_possibles) > 0:
# return True
#
# return False
def get_dernier_semestre_en_date(semestres: dict[int, FormSemestre]) -> FormSemestre:

View File

@ -44,6 +44,7 @@ Created on Fri Sep 9 09:15:05 2016
import io
import os
import time
from zipfile import ZipFile
import numpy as np
@ -69,14 +70,18 @@ class JuryPE(object):
diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
"""
def __init__(self, diplome):
def __init__(self, diplome: int):
pe_affichage.pe_start_log()
self.diplome = diplome
"L'année du diplome"
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 Sydans le jury
pe_affichage.pe_print(
f"Données de poursuite d'étude générées le {time.strftime('%d/%m/%Y à %H:%M')}\n"
)
# Chargement des étudiants à prendre en compte dans le jury
pe_affichage.pe_print(
f"""*** Recherche et chargement des étudiants diplômés en {
self.diplome}"""
@ -96,6 +101,8 @@ class JuryPE(object):
self._gen_xls_interclassements_rcss(zipfile)
self._gen_xls_synthese_jury_par_tag(zipfile)
self._gen_xls_synthese_par_etudiant(zipfile)
# et le log
self._add_log_to_zip(zipfile)
# Fin !!!! Tada :)
@ -251,6 +258,11 @@ class JuryPE(object):
zipfile, f"synthese_jury_{self.diplome}_par_etudiant.xlsx", output.read()
)
def _add_log_to_zip(self, zipfile):
"""Add a text file with the log messages"""
log_data = pe_affichage.pe_get_log()
self.add_file_to_zip(zipfile, "pe_log.txt", log_data)
def add_file_to_zip(self, zipfile: ZipFile, filename: str, data, path=""):
"""Add a file to given zip
All files under NOM_EXPORT_ZIP/
@ -397,7 +409,7 @@ class JuryPE(object):
# Les moys
champ = (descr, NOM_STAT_GROUPE, "moy")
moys = moy_traj.get_max()
moys = moy_traj.get_moy()
donnees.loc[etudids_communs, champ] = moys.loc[etudids_communs]
# Ajoute les données d'interclassement
@ -425,7 +437,7 @@ class JuryPE(object):
# Les moys
champ = (descr, nom_stat_promo, "moy")
moys = moy_interclass.get_max()
moys = moy_interclass.get_moy()
donnees.loc[etudids_communs, champ] = moys.loc[etudids_communs]
df_synthese = df_synthese.join(donnees)

View File

@ -76,6 +76,7 @@ class SemestreTag(TableTag):
# 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 notes, les modules implémentés triés, les étudiants, les coeffs,
# récupérés notamment de py:mod:`res_but`
@ -94,12 +95,12 @@ class SemestreTag(TableTag):
tags_personnalises = get_synthese_tags_personnalises_semestre(
self.nt.formsemestre
)
noms_tags_perso = list(set(tags_personnalises.keys()))
noms_tags_perso = sorted(list(set(tags_personnalises.keys())))
## Déduit des compétences
dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
noms_tags_comp = list(set(dict_ues_competences.values()))
noms_tags_auto = ["but"] + noms_tags_comp
# dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
# noms_tags_comp = list(set(dict_ues_competences.values()))
noms_tags_auto = ["but"] # + noms_tags_comp
self.tags = noms_tags_perso + noms_tags_auto
"""Tags du semestre taggué"""
@ -122,23 +123,31 @@ class SemestreTag(TableTag):
"""
raise ScoValueError(message)
ues_hors_sport = [ue for ue in self.ues if ue.type != UE_SPORT]
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
self.moyennes_tags = {}
for tag in tags_personnalises:
# pe_affichage.pe_print(f" -> Traitement du tag {tag}")
moy_gen_tag = self.compute_moyenne_tag(tag, tags_personnalises)
self.moyennes_tags[tag] = MoyenneTag(tag, moy_gen_tag)
moy_ues_tag = self.compute_moy_ues_tag(tag, tags_personnalises)
moy_gen_tag = self.compute_moy_gen_tag(moy_ues_tag)
# Ajoute les moyennes générales de BUT pour le semestre considéré
self.moyennes_tags[tag] = MoyenneTag(
tag, ues_hors_sport, moy_ues_tag, moy_gen_tag
)
# Ajoute les d'UE moyennes générales de BUT pour le semestre considéré
# moy_gen_but = self.nt.etud_moy_gen
# self.moyennes_tags["but"] = MoyenneTag("but", [], None, moy_gen_but, )
# Ajoute les moyennes par UEs (et donc par compétence) + la moyenne générale (but)
df_ues = pd.DataFrame({ue.id: self.nt.etud_moy_ue[ue.id] for ue in ues_hors_sport},
index = self.etudids)
# moy_ues = self.nt.etud_moy_ue[ue_id]
moy_gen_but = self.nt.etud_moy_gen
self.moyennes_tags["but"] = MoyenneTag("but", moy_gen_but)
self.moyennes_tags["but"] = MoyenneTag("but", ues_hors_sport, df_ues, moy_gen_but)
# Ajoute les moyennes par compétence
for ue_id, competence in dict_ues_competences.items():
if competence not in self.moyennes_tags:
moy_ue = self.nt.etud_moy_ue[ue_id]
self.moyennes_tags[competence] = MoyenneTag(competence, moy_ue)
self.tags_sorted = self.get_all_tags()
"""Tags (personnalisés+compétences) par ordre alphabétique"""
@ -156,8 +165,8 @@ 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, tags_infos: dict) -> pd.Series:
"""Calcule la moyenne des étudiants pour le tag indiqué,
def compute_moy_ues_tag(self, tag: str, tags_infos: dict) -> pd.Series:
"""Calcule la moyenne par UE des étudiants pour le tag indiqué,
pour ce SemestreTag, en ayant connaissance des informations sur
les tags (dictionnaire donnant les coeff de repondération)
@ -199,7 +208,12 @@ class SemestreTag(TableTag):
self.dispense_ues,
block=self.formsemestre.block_moyennes,
)
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
@ -207,7 +221,7 @@ class SemestreTag(TableTag):
# 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(
moyennes_ues_tag,
moy_ues_tag,
ects,
formation_id=self.formsemestre.formation_id,
skip_empty_ues=True,

View File

@ -42,19 +42,27 @@ import numpy as np
from app import ScoValueError
from app.comp.moy_sem import comp_ranks_series
from app.models import UniteEns
from app.pe import pe_affichage
from app.pe.pe_affichage import SANS_NOTE
from app.scodoc import sco_utils as scu
import pandas as pd
from app.scodoc.codes_cursus import UE_SPORT
TAGS_RESERVES = ["but"]
class MoyenneTag:
def __init__(self, tag: str, notes: pd.Series):
def __init__(
self,
tag: str,
ues: list[UniteEns],
notes_ues: pd.DataFrame,
notes_gen: pd.Series,
):
"""Classe centralisant la synthèse des moyennes/classements d'une série
d'étudiants à un tag donné, en stockant un dictionnaire :
d'étudiants à un tag donné, en stockant :
``
{
@ -69,16 +77,26 @@ class MoyenneTag:
Args:
tag: Un tag
note: Une série de notes (moyenne) sous forme d'un pd.Series()
ues: La liste des UEs ayant servie au calcul de la moyenne
notes_ues: Les moyennes (etudid x ues) aux différentes UEs et pour le tag
notes_gen: Une série de notes (moyenne) sous forme d'un pd.Series() (toutes UEs confondues)
"""
self.tag = tag
"""Le tag associé à la moyenne"""
self.etudids = list(notes.index) # calcul à venir
self.etudids = list(notes_gen.index) # calcul à venir
"""Les id des étudiants"""
self.inscrits_ids = notes[notes.notnull()].index.to_list()
"""Les id des étudiants dont la moyenne est non nulle"""
self.df: pd.DataFrame = self.comp_moy_et_stat(notes)
"""Le dataframe retraçant les moyennes/classements/statistiques"""
self.ues: list[UniteEns] = ues
"""Les UEs sur lesquelles sont calculées les moyennes"""
self.df_ues: dict[int, pd.DataFrame] = {}
"""Les dataframes retraçant les moyennes/classements/statistiques des étudiants aux UEs"""
for ue in self.ues: # if ue.type != UE_SPORT:
notes = notes_ues[ue.id]
self.df_ues[ue.id] = self.comp_moy_et_stat(notes)
self.inscrits_ids = notes_gen[notes_gen.notnull()].index.to_list()
"""Les id des étudiants dont la moyenne générale est non nulle"""
self.df_gen: pd.DataFrame = self.comp_moy_et_stat(notes_gen)
"""Le dataframe retraçant les moyennes/classements/statistiques général"""
self.synthese = self.to_dict()
"""La synthèse (dictionnaire) des notes/classements/statistiques"""
@ -88,7 +106,8 @@ class MoyenneTag:
def comp_moy_et_stat(self, notes: pd.Series) -> dict:
"""Calcule et structure les données nécessaires au PE pour une série
de notes (souvent une moyenne par tag) dans un dictionnaire spécifique.
de notes (pouvant être une moyenne d'un tag à une UE ou une moyenne générale
d'un tag) dans un dictionnaire spécifique.
Partant des notes, sont calculés les classements (en ne tenant compte
que des notes non nulles).
@ -121,64 +140,65 @@ class MoyenneTag:
# Les nb d'étudiants & nb d'inscrits
df["nb_etuds"] = len(self.etudids)
df.loc[self.inscrits_ids, "nb_inscrits"] = len(self.inscrits_ids)
# Les étudiants dont la note n'est pas nulle
inscrits_ids = notes[notes.notnull()].index.to_list()
df.loc[inscrits_ids, "nb_inscrits"] = len(inscrits_ids)
# Le classement des inscrits
notes_non_nulles = notes[self.inscrits_ids]
notes_non_nulles = notes[inscrits_ids]
(class_str, class_int) = comp_ranks_series(notes_non_nulles)
df.loc[self.inscrits_ids, "classement"] = class_int
df.loc[inscrits_ids, "classement"] = class_int
# Le rang (classement/nb_inscrit)
df["rang"] = df["rang"].astype(str)
df.loc[self.inscrits_ids, "rang"] = (
df.loc[self.inscrits_ids, "classement"].astype(int).astype(str)
df.loc[inscrits_ids, "rang"] = (
df.loc[inscrits_ids, "classement"].astype(int).astype(str)
+ "/"
+ df.loc[self.inscrits_ids, "nb_inscrits"].astype(int).astype(str)
+ df.loc[inscrits_ids, "nb_inscrits"].astype(int).astype(str)
)
# Les stat (des inscrits)
df.loc[self.inscrits_ids, "min"] = notes.min()
df.loc[self.inscrits_ids, "max"] = notes.max()
df.loc[self.inscrits_ids, "moy"] = notes.mean()
df.loc[inscrits_ids, "min"] = notes.min()
df.loc[inscrits_ids, "max"] = notes.max()
df.loc[inscrits_ids, "moy"] = notes.mean()
return df
def to_dict(self) -> dict:
"""Renvoie un dictionnaire de synthèse des moyennes/classements/statistiques"""
synthese = {
"notes": self.df["note"],
"classements": self.df["classement"],
"min": self.df["min"].mean(),
"max": self.df["max"].mean(),
"moy": self.df["moy"].mean(),
"nb_inscrits": self.df["nb_inscrits"].mean(),
"notes": self.df_gen["note"],
"classements": self.df_gen["classement"],
"min": self.df_gen["min"].mean(),
"max": self.df_gen["max"].mean(),
"moy": self.df_gen["moy"].mean(),
"nb_inscrits": self.df_gen["nb_inscrits"].mean(),
}
return synthese
def get_notes(self):
"""Série des notes, arrondies à 2 chiffres après la virgule"""
return self.df["note"].round(2)
return self.df_gen["note"].round(2)
def get_rangs_inscrits(self) -> pd.Series:
"""Série des rangs classement/nbre_inscrit"""
return self.df["rang"]
return self.df_gen["rang"]
def get_min(self) -> pd.Series:
"""Série des min"""
return self.df["min"].round(2)
return self.df_gen["min"].round(2)
def get_max(self) -> pd.Series:
"""Série des max"""
return self.df["max"].round(2)
return self.df_gen["max"].round(2)
def get_moy(self) -> pd.Series:
"""Série des moy"""
return self.df["moy"].round(2)
return self.df_gen["moy"].round(2)
def get_note_for_df(self, etudid: int):
"""Note d'un étudiant donné par son etudid"""
return round(self.df["note"].loc[etudid], 2)
return round(self.df_gen["note"].loc[etudid], 2)
def get_min_for_df(self) -> float:
"""Min renseigné pour affichage dans un df"""
@ -195,7 +215,7 @@ class MoyenneTag:
def get_class_for_df(self, etudid: int) -> str:
"""Classement ramené au nombre d'inscrits,
pour un étudiant donné par son etudid"""
classement = self.df["rang"].loc[etudid]
classement = self.df_gen["rang"].loc[etudid]
if not pd.isna(classement):
return classement
else:

View File

@ -72,14 +72,16 @@ def pe_view_sem_recap(formsemestre_id: int):
# Cosemestres diplomants
cosemestres = pe_comp.get_cosemestres_diplomants(annee_diplome)
cosemestres_tries = pe_comp.tri_semestres_par_rang(cosemestres)
affichage_cosemestres_tries = {rang: ", ".join([sem.titre_annee() for sem in cosemestres_tries[rang]]) for rang in cosemestres_tries}
if request.method == "GET":
return render_template(
"pe/pe_view_sem_recap.j2",
annee_diplome=annee_diplome,
formsemestre=formsemestre,
sco=ScoData(formsemestre=formsemestre),
cosemestres=cosemestres,
cosemestres=affichage_cosemestres_tries,
rangs_tries=sorted(affichage_cosemestres_tries.keys())
)
# request.method == "POST"
@ -102,11 +104,3 @@ def pe_view_sem_recap(formsemestre_id: int):
download_name=scu.sanitize_filename(jury.nom_export_zip + ".zip"),
as_attachment=True,
)
return render_template(
"pe/pe_view_sem_recap.j2",
annee_diplome=annee_diplome,
formsemestre=formsemestre,
sco=ScoData(formsemestre=formsemestre),
cosemestres=cosemestres,
)

View File

@ -396,7 +396,7 @@ class TF(object):
self.values[field] = int(self.values[field])
except ValueError:
msg.append(
f"valeur invalide ({self.values[field]}) pour le champs {field}"
f"valeur invalide ({self.values[field]}) pour le champ {field}"
)
ok = False
elif typ == "float" or typ == "real":
@ -404,7 +404,7 @@ class TF(object):
self.values[field] = float(self.values[field].replace(",", "."))
except ValueError:
msg.append(
f"valeur invalide ({self.values[field]}) pour le champs {field}"
f"valeur invalide ({self.values[field]}) pour le champ {field}"
)
ok = False
if ok:

View File

@ -265,7 +265,7 @@ def DBUpdateArgs(cnx, table, vals, where=None, commit=False, convert_empty_to_nu
# log('vals=%s\n'%vals)
except psycopg2.errors.StringDataRightTruncation as exc:
cnx.rollback()
raise ScoValueError("champs de texte trop long !") from exc
raise ScoValueError("champ de texte trop long !") from exc
except:
cnx.rollback() # get rid of this transaction
log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals))

View File

@ -166,9 +166,9 @@ def process_field(
values={pprint.pformat(cdict)}
"""
)
text = f"""<para><i>format invalide: champs</i> {missing_key} <i>inexistant !</i></para>"""
text = f"""<para><i>format invalide: champ</i> {missing_key} <i>inexistant !</i></para>"""
scu.flash_once(
f"Attention: format PDF invalide (champs {field}, clef {missing_key})"
f"Attention: format PDF invalide (champ {field}, clef {missing_key})"
)
raise
except: # pylint: disable=bare-except

View File

@ -126,53 +126,59 @@ def html_edit_formation_apc(
UniteEns.type != codes_cursus.UE_SPORT,
).first()
H += [
render_template(
"pn/form_mods.j2",
formation=formation,
titre=f"Ressources du S{semestre_idx}",
create_element_msg="créer une nouvelle ressource",
# matiere_parent=matiere_parent,
modules=ressources_in_sem,
module_type=ModuleType.RESSOURCE,
editable=editable,
tag_editable=tag_editable,
icons=icons,
scu=scu,
semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else "",
render_template(
"pn/form_mods.j2",
formation=formation,
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
create_element_msg="créer une nouvelle SAÉ",
# matiere_parent=matiere_parent,
modules=saes_in_sem,
module_type=ModuleType.SAE,
editable=editable,
tag_editable=tag_editable,
icons=icons,
scu=scu,
semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else "",
render_template(
"pn/form_mods.j2",
formation=formation,
titre=f"Autres modules (non BUT) du S{semestre_idx}",
create_element_msg="créer un nouveau module",
modules=other_modules_in_sem,
module_type=ModuleType.STANDARD,
editable=editable,
tag_editable=tag_editable,
icons=icons,
scu=scu,
semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else """<span class="fontred">créer une UE pour pouvoir ajouter des modules</span>""",
(
render_template(
"pn/form_mods.j2",
formation=formation,
titre=f"Ressources du S{semestre_idx}",
create_element_msg="créer une nouvelle ressource",
# matiere_parent=matiere_parent,
modules=ressources_in_sem,
module_type=ModuleType.RESSOURCE,
editable=editable,
tag_editable=tag_editable,
icons=icons,
scu=scu,
semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else ""
),
(
render_template(
"pn/form_mods.j2",
formation=formation,
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
create_element_msg="créer une nouvelle SAÉ",
# matiere_parent=matiere_parent,
modules=saes_in_sem,
module_type=ModuleType.SAE,
editable=editable,
tag_editable=tag_editable,
icons=icons,
scu=scu,
semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else ""
),
(
render_template(
"pn/form_mods.j2",
formation=formation,
titre=f"Autres modules (non BUT) du S{semestre_idx}",
create_element_msg="créer un nouveau module",
modules=other_modules_in_sem,
module_type=ModuleType.STANDARD,
editable=editable,
tag_editable=tag_editable,
icons=icons,
scu=scu,
semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else """<span class="fontred">créer une UE pour pouvoir ajouter des modules</span>"""
),
]
return "\n".join(H)
@ -202,7 +208,7 @@ def html_ue_infos(ue):
)
return render_template(
"pn/ue_infos.j2",
titre=f"UE {ue.acronyme} {ue.titre}",
titre=f"UE {ue.acronyme} {ue.titre or ''}",
ue=ue,
formsemestres=formsemestres,
nb_etuds_valid_ue=nb_etuds_valid_ue,

View File

@ -104,7 +104,7 @@ def matiere_create(ue_id=None):
default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
H = [
html_sco_header.sco_header(page_title="Création d'une matière"),
f"""<h2>Création d'une matière dans l'UE {ue.titre} ({ue.acronyme})</h2>
f"""<h2>Création d'une matière dans l'UE {ue.titre or ''} ({ue.acronyme})</h2>
<p class="help">Les matières sont des groupes de modules dans une UE
d'une formation donnée. Les matières servent surtout pour la
présentation (bulletins, etc) mais <em>n'ont pas de rôle dans le calcul

View File

@ -85,7 +85,7 @@ _moduleEditor = ndb.EditableTable(
"heures_tp": ndb.float_null_is_zero,
"numero": ndb.int_null_is_zero,
"coefficient": ndb.float_null_is_zero,
"module_type": ndb.int_null_is_zero
"module_type": ndb.int_null_is_zero,
#'ects' : ndb.float_null_is_null
},
)
@ -387,14 +387,16 @@ def module_edit(
"scodoc/help/modules.j2",
is_apc=is_apc,
semestre_id=semestre_id,
formsemestres=FormSemestre.query.filter(
ModuleImpl.formsemestre_id == FormSemestre.id,
ModuleImpl.module_id == module_id,
)
.order_by(FormSemestre.date_debut)
.all()
if not create
else None,
formsemestres=(
FormSemestre.query.filter(
ModuleImpl.formsemestre_id == FormSemestre.id,
ModuleImpl.module_id == module_id,
)
.order_by(FormSemestre.date_debut)
.all()
if not create
else None
),
create=create,
),
]
@ -413,9 +415,11 @@ def module_edit(
}
if module:
module_types |= {
scu.ModuleType(module.module_type)
if module.module_type
else scu.ModuleType.STANDARD
(
scu.ModuleType(module.module_type)
if module.module_type
else scu.ModuleType.STANDARD
)
}
# Numéro du module
# cherche le numero adéquat (pour placer le module en fin de liste)
@ -571,15 +575,17 @@ def module_edit(
"input_type": "menu",
"title": "Rattachement :" if is_apc else "Matière :",
"explanation": (
"UE de rattachement, utilisée notamment pour les malus"
+ (
" (module utilisé, ne peut pas être changé de semestre)"
if in_use
else ""
(
"UE de rattachement, utilisée notamment pour les malus"
+ (
" (module utilisé, ne peut pas être changé de semestre)"
if in_use
else ""
)
)
)
if is_apc
else "un module appartient à une seule matière.",
if is_apc
else "un module appartient à une seule matière."
),
"labels": mat_names,
"allowed_values": ue_mat_ids,
"enabled": unlocked,
@ -733,7 +739,7 @@ def module_edit(
"title": f"""<span class="fontred">{scu.EMO_WARNING }
L'UE <a class="stdlink" href="{
url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}">{ue.acronyme} {ue.titre}</a>
}">{ue.acronyme} {ue.titre or ''}</a>
n'est pas associée à un niveau de compétences
</span>""",
},
@ -766,12 +772,14 @@ def module_edit(
request.base_url,
scu.get_request_args(),
descr,
html_foot_markup=f"""<div class="sco_tag_module_edit"><span
html_foot_markup=(
f"""<div class="sco_tag_module_edit"><span
class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
>{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div>
"""
if not create
else "",
if not create
else ""
),
initvalues=module_dict if module else {},
submitlabel="Modifier ce module" if module else "Créer ce module",
cancelbutton="Annuler",
@ -814,7 +822,7 @@ def module_edit(
tf[2]["matiere_id"] = matiere.id
else:
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue.id, "titre": ue.titre, "numero": 1},
{"ue_id": ue.id, "titre": ue.titre or "", "numero": 1},
)
tf[2]["matiere_id"] = matiere_id

View File

@ -167,7 +167,7 @@ def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
if not ue.can_be_deleted():
raise ScoNonEmptyFormationObject(
f"UE (id={ue.id}, dud)",
msg=ue.titre,
msg=f"{ue.titre or ''} ({ue.acronyme})",
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
@ -639,8 +639,8 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
)
if not ue.can_be_deleted():
raise ScoNonEmptyFormationObject(
f"UE",
msg=ue.titre,
"UE",
msg=f"{ue.titre or ''} ({ue.acronyme})",
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
@ -651,7 +651,7 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
if not dialog_confirmed:
return scu.confirm_dialog(
f"<h2>Suppression de l'UE {ue.titre} ({ue.acronyme})</h2>",
f"<h2>Suppression de l'UE {ue.titre or ''} ({ue.acronyme})</h2>",
dest_url="",
parameters={"ue_id": ue.id},
cancel_url=url_for(
@ -1452,7 +1452,7 @@ def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None
H.append("<ul>")
for ue in ues:
H.append(
f"""<li>{ue.acronyme} ({ue.titre}) dans
f"""<li>{ue.acronyme} ({ue.titre or ''}) dans
<a class="stdlink" href="{
url_for("notes.ue_table",
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"

View File

@ -283,7 +283,7 @@ def evaluation_create_form(
"coef. mod.:" +str(coef_ue) if coef_ue
else "ce module n'a pas de coef. dans cette UE"
})</span>
<span class="eval_coef_ue_titre">{ue.titre}</span>
<span class="eval_coef_ue_titre">{ue.titre or ''}</span>
""",
"allow_null": False,
# ok si poids nul ou coef vers l'UE nul:

View File

@ -258,13 +258,17 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
submitlabel="Enregistrer ces validations",
cancelbutton="Annuler",
initvalues=initvalues,
cssclass="tf_ext_edit_ue_validations ext_apc"
if formsemestre.formation.is_apc()
else "tf_ext_edit_ue_validations",
cssclass=(
"tf_ext_edit_ue_validations ext_apc"
if formsemestre.formation.is_apc()
else "tf_ext_edit_ue_validations"
),
# En APC, stocke les coefficients pour l'affichage de la moyenne en direct
form_attrs=f"""data-ue_coefs='[{', '.join(str(ue.ects or 0) for ue in ues)}]'"""
if formsemestre.formation.is_apc()
else "",
form_attrs=(
f"""data-ue_coefs='[{', '.join(str(ue.ects or 0) for ue in ues)}]'"""
if formsemestre.formation.is_apc()
else ""
),
)
if tf[0] == -1:
return "<h4>annulation</h4>"
@ -421,12 +425,18 @@ def _ue_form_description(
"input_type": "text",
"size": 4,
"template": itemtemplate,
"title": "<tt>"
+ (f"S{ue.semestre_idx} " if ue.semestre_idx is not None else "")
+ f"<b>{ue.acronyme}</b></tt> {ue.titre}"
+ f" ({ue.ects} ECTS)"
if ue.ects is not None
else "",
"title": (
"<tt>"
+ (
f"S{ue.semestre_idx} "
if ue.semestre_idx is not None
else ""
)
+ f"<b>{ue.acronyme}</b></tt> {ue.titre or ''}"
+ f" ({ue.ects} ECTS)"
if ue.ects is not None
else ""
),
"attributes": [coef_disabled],
},
)

View File

@ -281,9 +281,11 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
menu_inscriptions = [
{
"title": "Gérer les inscriptions aux UE et modules"
if formsemestre.formation.is_apc()
else "Gérer les inscriptions aux modules",
"title": (
"Gérer les inscriptions aux UE et modules"
if formsemestre.formation.is_apc()
else "Gérer les inscriptions aux modules"
),
"endpoint": "notes.moduleimpl_inscriptions_stats",
"args": {"formsemestre_id": formsemestre_id},
}
@ -619,9 +621,9 @@ def formsemestre_description_table(
if ue.color:
for k in list(ue_info.keys()):
if not k.startswith("_"):
ue_info[
f"_{k}_td_attrs"
] = f'style="background-color: {ue.color} !important;"'
ue_info[f"_{k}_td_attrs"] = (
f'style="background-color: {ue.color} !important;"'
)
if not is_apc:
# n'affiche la ligne UE qu'en formation classique
# car l'UE de rattachement n'a pas d'intérêt en BUT
@ -1050,9 +1052,11 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
formsemestre_id=formsemestre_id, page_title="Tableau de bord"
),
formsemestre_warning_apc_setup(formsemestre, nt),
formsemestre_warning_etuds_sans_note(formsemestre, nt)
if can_change_all_notes
else "",
(
formsemestre_warning_etuds_sans_note(formsemestre, nt)
if can_change_all_notes
else ""
),
"""<p style="font-size: 130%"><b>Tableau de bord&nbsp;: </b>""",
]
if formsemestre.est_courant():
@ -1226,7 +1230,7 @@ def formsemestre_tableau_modules(
ue = modimpl.module.ue
if show_ues and (prev_ue_id != ue.id):
prev_ue_id = ue.id
titre = ue.titre
titre = ue.titre or ""
if use_ue_coefs:
titre += f""" <b>(coef. {ue.coefficient or 0.0})</b>"""
H.append(

View File

@ -728,7 +728,9 @@ def formsemestre_recap_parcours_table(
)
# Dispense BUT ?
if (etudid, ue.id) in nt.dispense_ues:
moy_ue_txt = "" if (ue_status and ue_status["is_capitalized"]) else ""
moy_ue_txt = (
"" if (ue_status and ue_status["is_capitalized"]) else ""
)
explanation_ue.append("non inscrit (dispense)")
else:
moy_ue_txt = scu.fmt_note(moy_ue)
@ -1098,7 +1100,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
ue_names = ["Choisir..."] + [
f"""{('S'+str(ue.semestre_idx)+' : ') if ue.semestre_idx is not None else ''
}{ue.acronyme} {ue.titre} ({ue.ue_code or ""})"""
}{ue.acronyme} {ue.titre or ''} ({ue.ue_code or ""})"""
for ue in ues
]
ue_ids = [""] + [ue.id for ue in ues]

View File

@ -494,7 +494,7 @@ def _normalize_apo_fields(infolist):
infolist: liste de dict renvoyés par le portail Apogee
recode les champs: paiementinscription (-> booleen), datefinalisationinscription (date)
ajoute le champs 'paiementinscription_str' : 'ok', 'Non' ou '?'
ajoute le champ 'paiementinscription_str' : 'ok', 'Non' ou '?'
ajoute les champs 'etape' (= None) et 'prenom' ('') s'ils ne sont pas présents.
ajoute le champ 'civilite_etat_civil' (=''), et 'prenom_etat_civil' (='') si non présent.
"""

View File

@ -342,13 +342,15 @@ def _build_page(
"\n".join(options),
"""</select>
""",
""
if read_only
else f"""
(
""
if read_only
else f"""
<input type="hidden" name="formsemestre_id" value="{sem['formsemestre_id']}"/>
<input type="submit" name="submitted" value="Appliquer les modifications"/>
&nbsp;<a href="#help">aide</a>
""",
"""
),
sco_inscr_passage.etuds_select_boxes(
etuds_by_cat,
sel_inscrits=False,
@ -356,9 +358,11 @@ def _build_page(
base_url=base_url,
read_only=read_only,
),
""
if read_only
else """<p/><input type="submit" name="submitted" value="Appliquer les modifications"/>""",
(
""
if read_only
else """<p/><input type="submit" name="submitted" value="Appliquer les modifications"/>"""
),
formsemestre_synchro_etuds_help(sem),
"""</form>""",
]
@ -420,9 +424,9 @@ def list_synch(sem, annee_apogee=None):
log(f"XXX key2etud etudid={etudid}, type {type(etudid)}")
etud = etuds[0]
etud["inscrit"] = is_inscrit # checkbox state
etud[
"datefinalisationinscription"
] = date_finalisation_inscr_by_nip.get(key, None)
etud["datefinalisationinscription"] = (
date_finalisation_inscr_by_nip.get(key, None)
)
if key in etudsapo_ident:
etud["etape"] = etudsapo_ident[key].get("etape", "")
else:
@ -855,7 +859,7 @@ def formsemestre_import_etud_admission(
if import_email:
if not "mail" in data_apo:
raise ScoValueError(
"la réponse portail n'a pas le champs requis 'mail'"
"la réponse portail n'a pas le champ requis 'mail'"
)
if (
adresse.email != data_apo["mail"]

View File

@ -1018,7 +1018,7 @@ def flash_errors(form):
"""Flashes form errors (version sommaire)"""
for field, errors in form.errors.items():
flash(
"Erreur: voir le champs %s" % (getattr(form, field).label.text,),
"Erreur: voir le champ %s" % (getattr(form, field).label.text,),
"warning",
)
# see https://getbootstrap.com/docs/4.0/components/alerts/

View File

@ -682,9 +682,9 @@ class RowRecap(tb.Row):
self.add_ue_modimpls_cols(ue, ue_status["is_capitalized"])
self.nb_ues_etud_parcours = len(res.etud_parcours_ues_ids(etud.id))
ue_valid_txt = (
ue_valid_txt_html
) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}"
ue_valid_txt = ue_valid_txt_html = (
f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}"
)
if self.nb_ues_warning:
ue_valid_txt_html += " " + scu.EMO_WARNING
cell_class = ""
@ -708,9 +708,9 @@ class RowRecap(tb.Row):
# sous-classé par JuryRow pour ajouter les codes
table: TableRecap = self.table
formsemestre: FormSemestre = table.res.formsemestre
table.group_titles[
"col_ue"
] = f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}"
table.group_titles["col_ue"] = (
f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}"
)
col_id = f"moy_ue_{ue.id}"
val = (
ue_status["moy"]
@ -740,7 +740,7 @@ class RowRecap(tb.Row):
)
table.foot_title_row.cells[col_id].target_attrs[
"title"
] = f"""{ue.titre} S{ue.semestre_idx or '?'}"""
] = f"""{ue.titre or ue.acronyme} S{ue.semestre_idx or '?'}"""
def add_ue_modimpls_cols(self, ue: UniteEns, is_capitalized: bool):
"""Ajoute à row les moyennes des modules (ou ressources et SAÉs) dans l'UE"""

View File

@ -49,7 +49,7 @@ table#edt2group tbody tr.active-row {
</div>
{% if ScoDocSiteConfig.get("edt_ics_group_field") %}
<div>Les groupes sont extrait du champs <b>{{ScoDocSiteConfig.get("edt_ics_group_field")}}</b>
<div>Les groupes sont extrait du champ <b>{{ScoDocSiteConfig.get("edt_ics_group_field")}}</b>
à l'aide de l'expression régulière: <tt>{{ScoDocSiteConfig.get("edt_ics_group_regexp")}}</tt>
</div>
{% else %}

View File

@ -43,12 +43,12 @@
<h3>Avis de poursuites d'études de la promo {{ annee_diplome }}</h3>
<div class="help">
Seront (a minima) pris en compte les étudiants ayant été inscrits aux semestres suivants :
Seront pris en compte les étudiants ayant été inscrits à l'un des semestres suivants :
<ul>
{% for fid in cosemestres %}
{% for rang in rangs_tries %}
<li>
{{ cosemestres[fid].titre_annee() }}
<strong>Semestre {{rang}}</strong> : {{ cosemestres[rang] }}
</li>
{% endfor %}
</ul>

View File

@ -1,7 +1,7 @@
{# Édition liste UEs APC #}
{% for semestre_idx in semestre_ids %}
<div class="formation_list_ues">
<div class="formation_list_ues_titre">Unités d'Enseignement
<div class="formation_list_ues_titre">Unités d'Enseignement
semestre {{semestre_idx}} &nbsp;-&nbsp; {{ects_by_sem[semestre_idx] | safe}} ECTS
</div>
<div class="formation_list_ues_content">
@ -9,14 +9,14 @@
{% for ue in ues_by_sem[semestre_idx] %}
<li class="notes_ue_list">
{% if editable and not loop.first %}
<a href="{{ url_for('notes.ue_move',
<a href="{{ url_for('notes.ue_move',
scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=0 )
}}" class="aud">{{icons.arrow_up|safe}}</a>
{% else %}
{{icons.arrow_none|safe}}
{% endif %}
{% if editable and not loop.last %}
<a href="{{ url_for('notes.ue_move',
<a href="{{ url_for('notes.ue_move',
scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=1 )
}}" class="aud">{{icons.arrow_down|safe}}</a>
{% else %}
@ -24,7 +24,7 @@
{% endif %}
</span>
<a class="smallbutton" href="{{ url_for('notes.ue_delete',
<a class="smallbutton" href="{{ url_for('notes.ue_delete',
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else
%}{{icons.delete_disabled|safe}}{% endif %}</a>
@ -34,12 +34,12 @@
ue.color if ue.color is not none else 'blue'}}"></span>
<b>{{ue.acronyme}} <a class="discretelink" href="{{
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}" title="{{ue.acronyme}}: {{
('pas de compétence associée'
if ue.niveau_competence is none
('pas de compétence associée'
if ue.niveau_competence is none
else 'compétence ' + ue.niveau_competence.annee + ' ' + ue.niveau_competence.competence.titre_long)
if ue.type == 0
else ''
}}">{{ue.titre}}</a>
}}">{{ue.titre or ue.acronyme}}</a>
</b>
{% set virg = joiner(", ") %}
<span class="ue_code">(
@ -66,7 +66,7 @@
</div>
{% endif %}
{% if editable and not ue.is_locked() %}
<a class="stdlink" href="{{ url_for('notes.ue_edit',
<a class="stdlink" href="{{ url_for('notes.ue_edit',
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}">modifier</a>
{% endif %}
@ -100,8 +100,8 @@
{% if editable %}
<ul>
<li class="notes_ue_list notes_ue_list_add"><a class="stdlink" href="{{
url_for('notes.ue_create',
scodoc_dept=g.scodoc_dept,
url_for('notes.ue_create',
scodoc_dept=g.scodoc_dept,
formation_id=formation.id,
default_semestre_idx=semestre_idx,
)}}">ajouter une UE</a>

View File

@ -4,7 +4,7 @@
{% block app_content %}
<!-- begin ue_infos -->
<h2>Unité d'Enseignement {{ue.acronyme|e}} {{ue.titre}}</h2>
<h2>Unité d'Enseignement {{ue.acronyme|e}} {{(ue.titre or '')|e}}</h2>
<div class="ue_infos">
@ -36,7 +36,7 @@
{% if loop.first %}
<ul>
{% endif %}
<li><a href="{{url_for('notes.formsemestre_status',
<li><a href="{{url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=sem.id )}}">{{sem.titre_mois()}}</a></li>
{% if loop.last %}
</ul>

View File

@ -2169,9 +2169,6 @@ def _module_selector_multiple(
return render_template(
"assiduites/widgets/moduleimpl_selector_multiple.j2",
choices=choices,
formsemestre_id=(
only_form.id if only_form else list(modimpls_by_formsemestre.keys())[0]
),
moduleimpl_id=moduleimpl_id,
)

View File

@ -113,7 +113,7 @@ def table_modules_ue_coefs(formation_id, semestre_idx=None, parcours_id: int = N
"y": 1, # 1ere ligne
"style": "title_ue",
"data": ue.acronyme,
"title": ue.titre,
"title": ue.titre or ue.acronymexs,
}
for (col, ue) in enumerate(ues, start=2)
]
@ -214,11 +214,13 @@ def edit_modules_ue_coefs():
{lockicon}
</h2>
""",
"""<span class="warning">Formation verrouilée car un ou plusieurs
(
"""<span class="warning">Formation verrouilée car un ou plusieurs
semestres verrouillés l'utilisent.
</span>"""
if locked
else "",
if locked
else ""
),
render_template(
"pn/form_modules_ue_coefs.j2",
formation=formation,

View File

@ -61,11 +61,11 @@ class DevConfig(Config):
DEBUG = True
TESTING = False
SQLALCHEMY_DATABASE_URI = (
os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC_DEV"
os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC"
)
SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a"
# pour le avoir url_for dans le shell:
# SERVER_NAME = os.environ.get("SCODOC_TEST_SERVER_NAME") or "localhost"
# SERVER_NAME = "http://localhost:8080"
class TestConfig(DevConfig):

View File

@ -0,0 +1,56 @@
"""etudiant_annotations : ajoute clé externe etudiant et moduleimpl
Revision ID: 2e4875004e12
Revises: 3fa988ff8970
Create Date: 2024-02-11 12:10:36.743212
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "2e4875004e12"
down_revision = "3fa988ff8970"
branch_labels = None
depends_on = None
def upgrade():
# Supprime les annotations orphelines
op.execute(
"""DELETE FROM etud_annotations
WHERE etudid NOT IN (SELECT id FROM identite);
"""
)
# Ajoute clé:
with op.batch_alter_table("etud_annotations", schema=None) as batch_op:
batch_op.create_foreign_key(None, "identite", ["etudid"], ["id"])
# Et modif liée au commit 072d013590abf715395bc987fb48de49f6750527
with op.batch_alter_table("notes_moduleimpl", schema=None) as batch_op:
batch_op.drop_constraint(
"notes_moduleimpl_responsable_id_fkey", type_="foreignkey"
)
batch_op.create_foreign_key(
None, "user", ["responsable_id"], ["id"], ondelete="SET NULL"
)
# cet index en trop trainait depuis longtemps...
with op.batch_alter_table("assiduites", schema=None) as batch_op:
batch_op.drop_index("ix_assiduites_user_id")
def downgrade():
with op.batch_alter_table("notes_moduleimpl", schema=None) as batch_op:
batch_op.drop_constraint(None, type_="foreignkey")
batch_op.create_foreign_key(
"notes_moduleimpl_responsable_id_fkey", "user", ["responsable_id"], ["id"]
)
with op.batch_alter_table("etud_annotations", schema=None) as batch_op:
batch_op.drop_constraint(None, type_="foreignkey")
with op.batch_alter_table("assiduites", schema=None) as batch_op:
batch_op.create_index("ix_assiduites_user_id", ["user_id"], unique=False)

View File

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

View File

@ -11,14 +11,12 @@ Usage: pytest tests/scenarios/test_scenario1_formation.py
"""
# code écrit par Fares Amer, mai 2021 et porté sur ScoDoc 8 en août 2021
import random
from tests.unit import sco_fake_gen
from app.scodoc import sco_edit_module
from app.scodoc import sco_formations
from app.scodoc import sco_moduleimpl
@pytest.mark.skip # test obsolete
def test_scenario1(test_client):
"""Applique "scenario 1"""
run_scenario1()
@ -28,7 +26,9 @@ def run_scenario1():
G = sco_fake_gen.ScoFake(verbose=False)
# Lecture fichier XML local:
with open("tests/unit/formation-exemple-1.xml") as f:
with open(
"tests/ressources/formations/formation-exemple-1.xml", encoding="utf8"
) as f:
doc = f.read()
# --- Création de la formation