Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into entreprises

This commit is contained in:
Arthur ZHU 2022-07-20 18:24:24 +02:00
commit 3a7879c8aa
30 changed files with 565 additions and 303 deletions

View File

@ -68,7 +68,7 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
options_str = "\n".join(options) options_str = "\n".join(options)
return f""" return f"""
<div class="ue_choix_niveau"> <div class="ue_choix_niveau">
<form id="form_ue_choix_niveau"> <form class="form_ue_choix_niveau">
<b>Niveau de compétence associé:</b> <b>Niveau de compétence associé:</b>
<select onchange="set_ue_niveau_competence(this);" <select onchange="set_ue_niveau_competence(this);"
data-ue_id="{ue.id}" data-ue_id="{ue.id}"

View File

@ -321,8 +321,8 @@ class BulletinBUT:
} }
decisions_ues = self.res.get_etud_decision_ues(etud.id) or {} decisions_ues = self.res.get_etud_decision_ues(etud.id) or {}
if self.prefs["bul_show_ects"]: if self.prefs["bul_show_ects"]:
ects_tot = sum([ue.ects or 0 for ue in res.ues]) if res.ues else 0.0 ects_tot = res.etud_ects_tot_sem(etud.id)
ects_acquis = sum([d.get("ects", 0) for d in decisions_ues.values()]) ects_acquis = res.get_etud_ects_valides(etud.id, decisions_ues)
semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot} semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
if sco_preferences.get_preference("bul_show_decision", formsemestre.id): if sco_preferences.get_preference("bul_show_decision", formsemestre.id):
semestre_infos.update( semestre_infos.update(

View File

@ -160,6 +160,9 @@ class DecisionsProposees:
"Validation enregistrée" "Validation enregistrée"
self.code_valide: str = code_valide self.code_valide: str = code_valide
"Code décision actuel enregistré" "Code décision actuel enregistré"
# S'assure que le code enregistré est toujours présent dans le menu
if self.code_valide and self.code_valide not in self.codes:
self.codes.append(self.code_valide)
self.explanation: str = explanation self.explanation: str = explanation
"Explication à afficher à côté de la décision" "Explication à afficher à côté de la décision"
self.recorded = False self.recorded = False
@ -784,6 +787,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
self.code_valide = self.validation.code self.code_valide = self.validation.code
if rcue.est_compensable(): if rcue.est_compensable():
self.codes.insert(0, sco_codes.CMP) self.codes.insert(0, sco_codes.CMP)
# les interprétations varient, on autorise aussi ADM:
self.codes.insert(1, sco_codes.ADM)
elif rcue.est_validable(): elif rcue.est_validable():
self.codes.insert(0, sco_codes.ADM) self.codes.insert(0, sco_codes.ADM)
else: else:
@ -885,11 +890,21 @@ class DecisionsProposeesUE(DecisionsProposees):
ue: UniteEns, ue: UniteEns,
inscription_etat: str = scu.INSCRIT, inscription_etat: str = scu.INSCRIT,
): ):
super().__init__(etud=etud) # Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
# mais ici on a restreint au formsemestre donc une seule (prend la première)
self.validation = ScolarFormSemestreValidation.query.filter_by(
etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id
).first()
super().__init__(
etud=etud,
code_valide=self.validation.code if self.validation is not None else None,
)
self.formsemestre = formsemestre self.formsemestre = formsemestre
self.ue: UniteEns = ue self.ue: UniteEns = ue
self.rcue: RegroupementCoherentUE = None self.rcue: RegroupementCoherentUE = None
"Le rcu auquel est rattaché cette UE, ou None" "Le rcue auquel est rattaché cette UE, ou None"
self.inscription_etat = inscription_etat self.inscription_etat = inscription_etat
"inscription: I, DEM, DEF" "inscription: I, DEM, DEF"
if ue.type == sco_codes.UE_SPORT: if ue.type == sco_codes.UE_SPORT:
@ -904,13 +919,6 @@ class DecisionsProposeesUE(DecisionsProposees):
] ]
self.moy_ue = "-" self.moy_ue = "-"
return return
# Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
# mais ici on a restreint au formsemestre donc une seule (prend la première)
self.validation = ScolarFormSemestreValidation.query.filter_by(
etudid=self.etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id
).first()
if self.validation is not None:
self.code_valide = self.validation.code
# Moyenne de l'UE ? # Moyenne de l'UE ?
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)

View File

@ -38,7 +38,7 @@ def _descr_cursus_but(etud: Identite) -> str:
return ", ".join(f"S{indice}" for indice in indices) return ", ".join(f"S{indice}" for indice in indices)
def pvjury_table_but(formsemestre_id: int, format="html") -> list[dict]: def pvjury_table_but(formsemestre_id: int, format="html"):
"""Page récapitulant les décisions de jury BUT """Page récapitulant les décisions de jury BUT
formsemestre peut être pair ou impair formsemestre peut être pair ou impair
""" """

View File

@ -395,6 +395,18 @@ def get_table_jury_but(
row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id]) row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id])
row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id]) row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id])
row.add_rcue_cells(dec_rcue) row.add_rcue_cells(dec_rcue)
# --- Les ECTS validés
ects_valides = 0.0
if deca.res_impair:
ects_valides += deca.res_impair.get_etud_ects_valides(etudid)
if deca.res_pair:
ects_valides += deca.res_pair.get_etud_ects_valides(etudid)
row.add_cell(
"ects_annee",
"ECTS",
f"""{int(ects_valides)}""",
"col_code_annee",
)
# --- Le code annuel existant # --- Le code annuel existant
row.add_cell( row.add_cell(
"code_annee", "code_annee",

View File

@ -607,6 +607,28 @@ class BonusCachan1(BonusSportAdditif):
self.bonus_ues[ue.id] = 0.0 # annule self.bonus_ues[ue.id] = 0.0 # annule
class BonusCaen(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de Caen Normandie.
Les étudiants de l'IUT de Caen Normandie peuvent suivre des enseignements
optionnels non rattachés à une unité d'enseignement:
<ul>
<li><b>Sport</b>.
<li><b>Engagement étudiant</b>
</ul>
Les points au-dessus de 10 sur 20 obtenus dans chacune de ces matières
optionnelles sont cumulés et donnent lieu à un bonus sur chaque UE de 5%
des points au dessus de 10 (soit +0,1 point pour chaque tranche de 2 points au
dessus de 10).
"""
name = "bonus_caen"
displayed_name = "IUT de Caen Normandie"
bonus_max = 1.0
seuil_moy_gen = 10.0 # au dessus de 10
proportion_point = 0.05 # 5%
class BonusCalais(BonusSportAdditif): class BonusCalais(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT LCO. """Calcul bonus modules optionnels (sport, culture), règle IUT LCO.
@ -955,20 +977,53 @@ class BonusLimousin(BonusSportAdditif):
bonus_max = 0.5 bonus_max = 0.5
class BonusLyonProvisoire(BonusSportAdditif): class BonusLyon(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture) à l'IUT de Lyon (provisoire) """Calcul bonus modules optionnels (sport, culture) à l'IUT de Lyon (2022)
<p><b>À partir de 2022-2023 :</b>
des points de bonification seront ajoutés aux moyennes de toutes les UE
du semestre concerné (3/100e de point par point au-dessus de 10).<br>
Cette bonification ne pourra excéder 1/2 point sur chacune des UE
</p>
<ul>
<li>Exemple 1 :<br>
<tt>
Sport 12/20 => +0.06<br>
LV2 13/20 => +0.09<br>
Bonus total = +0.15 appliqué à toutes les UE du semestre
</tt>
</li>
<li>Exemple 2 :<br>
<tt>
Sport 20/20 => +0.30<br>
LV2 18/20 => +0.24<br>
Bonus total = +0.50 appliqué à toutes les UE du semestre
</tt></li>
</ul>
<p><b>Jusqu'en 2021-2022 :</b>
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés et 1,8% de ces points cumulés optionnelles sont cumulés et 1,8% de ces points cumulés
s'ajoutent aux moyennes, dans la limite d'1/2 point. s'ajoutent aux moyennes générales, dans la limite d'1/2 point.
</p>
""" """
name = "bonus_lyon_provisoire" name = "bonus_lyon_provisoire"
displayed_name = "IUT de Lyon (provisoire)" displayed_name = "IUT de Lyon"
seuil_moy_gen = 10.0 # points comptés au dessus de 10. seuil_moy_gen = 10.0 # points comptés au dessus de 10.
proportion_point = 0.018
bonus_max = 0.5 bonus_max = 0.5
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
if self.formsemestre.date_debut > datetime.date(2022, 8, 1):
self.classic_use_bonus_ues = True # pour les LP
self.proportion_point = 0.03
else:
self.classic_use_bonus_ues = False
self.proportion_point = 0.018
return super().compute_bonus(
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
)
class BonusMantes(BonusSportAdditif): class BonusMantes(BonusSportAdditif):
"""Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines. """Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines.

View File

@ -32,7 +32,7 @@ class ValidationsSemestre(ResultatsCache):
{ etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}""" { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}"""
self.decisions_jury_ues = {} self.decisions_jury_ues = {}
"""Décisions sur des UEs dans ce semestre: """Décisions sur des UEs dans ce semestre:
{ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}} { etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date': "d/m/y", "ects" : x}}}
""" """
self.ue_capitalisees: pd.DataFrame = None self.ue_capitalisees: pd.DataFrame = None
"""DataFrame, index etudid """DataFrame, index etudid
@ -55,7 +55,9 @@ class ValidationsSemestre(ResultatsCache):
"""Cherche les decisions du jury pour le semestre (pas les UE). """Cherche les decisions du jury pour le semestre (pas les UE).
Calcule les attributs: Calcule les attributs:
decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }} decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}} decision_jury_ues={ etudid :
{ ue_id : { 'code' : Note|ADM|CMP, 'event_date' : "d/m/y", 'ects' : x }}
}
Si la décision n'a pas été prise, la clé etudid n'est pas présente. Si la décision n'a pas été prise, la clé etudid n'est pas présente.
Si l'étudiant est défaillant, pas de décisions d'UE. Si l'étudiant est défaillant, pas de décisions d'UE.
""" """

View File

@ -6,7 +6,6 @@
"""Résultats semestres BUT """Résultats semestres BUT
""" """
from collections.abc import Generator
from re import U from re import U
import time import time
import numpy as np import numpy as np
@ -235,7 +234,3 @@ class ResultatsSemestreBUT(NotesTableCompat):
""" """
s = self.ues_inscr_parcours_df.loc[etudid] s = self.ues_inscr_parcours_df.loc[etudid]
return s.index[s.notna()] return s.index[s.notna()]
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
"""Liste des UE auxquelles l'étudiant est inscrit."""
return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid))

View File

@ -8,6 +8,7 @@
""" """
from collections import Counter from collections import Counter
from collections.abc import Generator
from functools import cached_property from functools import cached_property
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -120,6 +121,15 @@ class ResultatsSemestre(ResultatsCache):
# car tous les étudiants sont inscrits à toutes les UE # car tous les étudiants sont inscrits à toutes les UE
return [ue.id for ue in self.ues if ue.type != UE_SPORT] return [ue.id for ue in self.ues if ue.type != UE_SPORT]
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
"""Liste des UE auxquelles l'étudiant est inscrit."""
return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid))
def etud_ects_tot_sem(self, etudid: int) -> float:
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
etud_ues = self.etud_ues(etudid)
return sum([ue.ects or 0 for ue in etud_ues]) if etud_ues else 0.0
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray: def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue. """Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
Utile pour stats bottom tableau recap. Utile pour stats bottom tableau recap.
@ -167,7 +177,7 @@ class ResultatsSemestre(ResultatsCache):
Rappel: l'étudiant est inscrit à des modimpls et non à des UEs. Rappel: l'étudiant est inscrit à des modimpls et non à des UEs.
- En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules - En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules
du parcours. XXX notion à implémenter, pour l'instant toutes les UE du semestre. du parcours.
- En classique: toutes les UEs des modimpls auxquels l'étudiant est inscrit sont - En classique: toutes les UEs des modimpls auxquels l'étudiant est inscrit sont
susceptibles d'être validées. susceptibles d'être validées.
@ -175,9 +185,8 @@ class ResultatsSemestre(ResultatsCache):
Les UE "bonus" (sport) ne sont jamais "validables". Les UE "bonus" (sport) ne sont jamais "validables".
""" """
if self.is_apc: if self.is_apc:
# TODO: introduire la notion de parcours (#sco93) return list(self.etud_ues(etudid))
return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all() # Formations classiques:
else:
# restreint aux UE auxquelles l'étudiant est inscrit (dans l'un des modimpls) # restreint aux UE auxquelles l'étudiant est inscrit (dans l'un des modimpls)
ues = { ues = {
modimpl.module.ue modimpl.module.ue
@ -192,7 +201,7 @@ class ResultatsSemestre(ResultatsCache):
Utile en formations classiques, surchargée pour le BUT. Utile en formations classiques, surchargée pour le BUT.
Inclus modules bonus le cas échéant. Inclus modules bonus le cas échéant.
""" """
# sert pour l'affichage ou non de l'UE sur le bulletin # Utilisée pour l'affichage ou non de l'UE sur le bulletin
# Méthode surchargée en BUT # Méthode surchargée en BUT
modimpls = [ modimpls = [
modimpl modimpl
@ -315,6 +324,8 @@ class ResultatsSemestre(ResultatsCache):
"formsemestre_id": None, "formsemestre_id": None,
"capitalized_ue_id": None, "capitalized_ue_id": None,
"ects_pot": 0.0, "ects_pot": 0.0,
"ects": 0.0,
"ects_ue": ue.ects,
} }
if not ue_id in self.etud_moy_ue: if not ue_id in self.etud_moy_ue:
return None return None
@ -369,6 +380,12 @@ class ResultatsSemestre(ResultatsCache):
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external, "is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
"coef_ue": coef_ue, "coef_ue": coef_ue,
"ects_pot": ue.ects or 0.0, "ects_pot": ue.ects or 0.0,
"ects": self.validations.decisions_jury_ues.get(etudid, {})
.get(ue.id, {})
.get("ects", 0.0)
if self.validations.decisions_jury_ues
else 0.0,
"ects_ue": ue.ects,
"cur_moy_ue": cur_moy_ue, "cur_moy_ue": cur_moy_ue,
"moy": moy_ue, "moy": moy_ue,
"event_date": ue_cap["event_date"] if is_capitalized else None, "event_date": ue_cap["event_date"] if is_capitalized else None,

View File

@ -278,7 +278,7 @@ class NotesTableCompat(ResultatsSemestre):
def get_etud_decision_ues(self, etudid: int) -> dict: def get_etud_decision_ues(self, etudid: int) -> dict:
"""Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu. """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
Ne tient pas compte des UE capitalisées. Ne tient pas compte des UE capitalisées.
{ ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : } { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : "d/m/y", 'ects' : x }
Ne renvoie aucune decision d'UE pour les défaillants Ne renvoie aucune decision d'UE pour les défaillants
""" """
if self.get_etud_etat(etudid) == DEF: if self.get_etud_etat(etudid) == DEF:
@ -290,6 +290,17 @@ class NotesTableCompat(ResultatsSemestre):
) )
return self.validations.decisions_jury_ues.get(etudid, None) return self.validations.decisions_jury_ues.get(etudid, None)
def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> 0:
"""Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre.
NB: avant jury, rien d'enregistré, donc zéro ECTS.
Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decision_ues()
"""
if decisions_ues is False:
decisions_ues = self.get_etud_decision_ues(etudid)
if not decisions_ues:
return 0.0
return sum([d.get("ects", 0.0) for d in decisions_ues.values()])
def get_etud_decision_sem(self, etudid: int) -> dict: def get_etud_decision_sem(self, etudid: int) -> dict:
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu. """Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id } { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
@ -322,7 +333,7 @@ class NotesTableCompat(ResultatsSemestre):
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl """La moyenne de l'étudiant dans le moduleimpl
En APC, il s'agira d'une moyenne indicative sans valeur. En APC, il s'agira d'une moyenne indicative sans valeur.
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM) Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM)
""" """
raise NotImplementedError() # virtual method raise NotImplementedError() # virtual method
@ -340,24 +351,25 @@ class NotesTableCompat(ResultatsSemestre):
ects_pot : (float) nb de crédits ECTS qui seraient validés ects_pot : (float) nb de crédits ECTS qui seraient validés
(sous réserve de validation par le jury) (sous réserve de validation par le jury)
ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives) ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
ects_total: (float) total des ECTS validables
Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non Les ects_pot sont les ECTS des UE au dessus de la barre (10/20 en principe),
encore enregistrées). avant le jury (donc non encore enregistrés).
""" """
# was nt.get_etud_moy_infos # was nt.get_etud_moy_infos
# XXX pour compat nt, à remplacer ultérieurement # XXX pour compat nt, à remplacer ultérieurement
ues = self.get_etud_ue_validables(etudid) ues = self.get_etud_ue_validables(etudid)
ects_pot = 0.0 ects_pot = 0.0
ects_total = 0.0
for ue in ues: for ue in ues:
if ( if ue.id in self.etud_moy_ue and ue.ects is not None:
ue.id in self.etud_moy_ue ects_total += ue.ects
and ue.ects is not None if self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE:
and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE
):
ects_pot += ue.ects ects_pot += ue.ects
return { return {
"ects_pot": ects_pot, "ects_pot": ects_pot,
"ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé) "ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé)
"ects_total": ects_total,
} }
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]: def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:

View File

@ -105,9 +105,11 @@ class Formation(db.Model):
return len(self.formsemestres.filter_by(etat=False).all()) > 0 return len(self.formsemestres.filter_by(etat=False).all()) > 0
def invalidate_module_coefs(self, semestre_idx: int = None): def invalidate_module_coefs(self, semestre_idx: int = None):
"""Invalide les coefficients de modules cachés. """Invalide le cache des coefficients de modules.
Si semestre_idx est None, invalide tous les semestres, Si semestre_idx est None, invalide les coefs de tous les semestres,
sinon invalide le semestre indiqué et le cache de la formation. sinon invalide le semestre indiqué et le cache de la formation.
Dans tous les cas, invalide tous les formsemestres.
""" """
if semestre_idx is None: if semestre_idx is None:
keys = {f"{self.id}.{m.semestre_id}" for m in self.modules} keys = {f"{self.id}.{m.semestre_id}" for m in self.modules}

View File

@ -83,10 +83,9 @@ class UniteEns(db.Model):
return sco_edit_ue.ue_is_locked(self.id) return sco_edit_ue.ue_is_locked(self.id)
def can_be_deleted(self) -> bool: def can_be_deleted(self) -> bool:
"""True si l'UE n'est pas utilisée dans des formsemestre """True si l'UE n'a pas de moduleimpl rattachés
et n'a pas de module rattachés (pas un seul module de cette UE n'a de modimpl)
""" """
# "pas un seul module de cette UE n'a de modimpl...""
return (self.modules.count() == 0) or not any( return (self.modules.count() == 0) or not any(
m.modimpls.all() for m in self.modules m.modimpls.all() for m in self.modules
) )

View File

@ -433,8 +433,6 @@ class ApoEtud(dict):
return VOID_APO_RES return VOID_APO_RES
# Elements UE # Elements UE
# if etudid == 3661 and nt.formsemestre.semestre_id == 2: # XXX XXX XXX
# breakpoint()
decisions_ue = nt.get_etud_decision_ues(etudid) decisions_ue = nt.get_etud_decision_ues(etudid)
for ue in nt.get_ues_stat_dict(): for ue in nt.get_ues_stat_dict():
if ue["code_apogee"] and code in { if ue["code_apogee"] and code in {

View File

@ -65,6 +65,7 @@ from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from config import Config from config import Config
from app import log from app import log
from app.but import jury_but_pv
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import Departement, FormSemestre from app.models import Departement, FormSemestre
@ -361,8 +362,14 @@ def do_formsemestre_archive(
data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder) data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
if data: if data:
PVArchive.store(archive_id, "Bulletins.json", data_js) PVArchive.store(archive_id, "Bulletins.json", data_js)
# Decisions de jury, en XLS # Décisions de jury, en XLS
data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False) if formsemestre.formation.is_apc():
response = jury_but_pv.pvjury_table_but(formsemestre_id, format="xls")
data = response.get_data()
else: # formations classiques
data = sco_pvjury.formsemestre_pvjury(
formsemestre_id, format="xls", publish=False
)
if data: if data:
PVArchive.store( PVArchive.store(
archive_id, archive_id,
@ -385,7 +392,10 @@ def do_formsemestre_archive(
) )
if data: if data:
PVArchive.store(archive_id, "CourriersDecisions%s.pdf" % groups_filename, data) PVArchive.store(archive_id, "CourriersDecisions%s.pdf" % groups_filename, data)
# PV de jury (PDF):
# PV de jury (PDF): disponible seulement en classique
# en BUT, le PV est sous forme excel (Decisions_Jury.xlsx ci-dessus)
if not formsemestre.formation.is_apc():
dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True) dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True)
data = sco_pvpdf.pvjury_pdf( data = sco_pvpdf.pvjury_pdf(
dpv, dpv,

View File

@ -757,15 +757,16 @@ def etud_descr_situation_semestre(
if show_date_inscr: if show_date_inscr:
if not date_inscr: if not date_inscr:
infos["date_inscription"] = "" infos["date_inscription"] = ""
infos["descr_inscription"] = f"Pas inscrit{ne}." infos["descr_inscription"] = f"Pas inscrit{ne}"
else: else:
infos["date_inscription"] = date_inscr infos["date_inscription"] = date_inscr
infos["descr_inscription"] = f"Inscrit{ne} le {date_inscr}." infos["descr_inscription"] = f"Inscrit{ne} le {date_inscr}"
else: else:
infos["date_inscription"] = "" infos["date_inscription"] = ""
infos["descr_inscription"] = "" infos["descr_inscription"] = ""
infos["situation"] = infos["descr_inscription"] infos["descr_defaillance"] = ""
# Décision: valeurs par defaut vides: # Décision: valeurs par defaut vides:
infos["decision_jury"] = infos["descr_decision_jury"] = "" infos["decision_jury"] = infos["descr_decision_jury"] = ""
infos["decision_sem"] = "" infos["decision_sem"] = ""
@ -777,13 +778,14 @@ def etud_descr_situation_semestre(
infos["descr_demission"] = f"Démission le {date_dem}." infos["descr_demission"] = f"Démission le {date_dem}."
infos["date_demission"] = date_dem infos["date_demission"] = date_dem
infos["decision_jury"] = infos["descr_decision_jury"] = "Démission" infos["decision_jury"] = infos["descr_decision_jury"] = "Démission"
infos["situation"] += " " + infos["descr_demission"] infos["situation"] = ". ".join(
[x for x in [infos["descr_inscription"], infos["descr_demission"]] if x]
)
return infos, None # ne donne pas les dec. de jury pour les demissionnaires return infos, None # ne donne pas les dec. de jury pour les demissionnaires
if date_def: if date_def:
infos["descr_defaillance"] = f"Défaillant{ne}" infos["descr_defaillance"] = f"Défaillant{ne}"
infos["date_defaillance"] = date_def infos["date_defaillance"] = date_def
infos["descr_decision_jury"] = f"Défaillant{ne}" infos["descr_decision_jury"] = f"Défaillant{ne}"
infos["situation"] += " " + infos["descr_defaillance"]
dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid]) dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid])
if dpv: if dpv:
@ -794,28 +796,27 @@ def etud_descr_situation_semestre(
# Décisions de jury: # Décisions de jury:
pv = dpv["decisions"][0] pv = dpv["decisions"][0]
dec = "" descr_dec = ""
if pv["decision_sem_descr"]: if pv["decision_sem_descr"]:
infos["decision_jury"] = pv["decision_sem_descr"] infos["decision_jury"] = pv["decision_sem_descr"]
infos["descr_decision_jury"] = ( infos["descr_decision_jury"] = "Décision jury: " + pv["decision_sem_descr"]
"Décision jury: " + pv["decision_sem_descr"] + ". " descr_dec = infos["descr_decision_jury"]
)
dec = infos["descr_decision_jury"]
else: else:
infos["descr_decision_jury"] = "" infos["descr_decision_jury"] = ""
infos["decision_jury"] = "" infos["decision_jury"] = ""
if pv["decisions_ue_descr"] and show_uevalid: if pv["decisions_ue_descr"] and show_uevalid:
infos["decisions_ue"] = pv["decisions_ue_descr"] infos["decisions_ue"] = pv["decisions_ue_descr"]
infos["descr_decisions_ue"] = " UE acquises: " + pv["decisions_ue_descr"] + ". " infos["descr_decisions_ue"] = " UE acquises: " + pv["decisions_ue_descr"]
dec += infos["descr_decisions_ue"]
else: else:
infos["decisions_ue"] = "" infos["decisions_ue"] = ""
infos["descr_decisions_ue"] = "" infos["descr_decisions_ue"] = ""
infos["mention"] = pv["mention"] infos["mention"] = pv["mention"]
if pv["mention"] and show_mention: if pv["mention"] and show_mention:
dec += f"Mention {pv['mention']}." descr_mention = f"Mention {pv['mention']}"
else:
descr_mention = ""
# Décisions APC / BUT # Décisions APC / BUT
if pv.get("decision_annee", {}): if pv.get("decision_annee", {}):
@ -828,17 +829,44 @@ def etud_descr_situation_semestre(
infos["descr_decisions_rcue"] = pv.get("descr_decisions_rcue", "") infos["descr_decisions_rcue"] = pv.get("descr_decisions_rcue", "")
infos["descr_decisions_niveaux"] = pv.get("descr_decisions_niveaux", "") infos["descr_decisions_niveaux"] = pv.get("descr_decisions_niveaux", "")
infos["situation"] += " " + dec descr_autorisations = ""
if not pv["validation_parcours"]: # parcours non terminé if not pv["validation_parcours"]: # parcours non terminé
if pv["autorisations_descr"]: if pv["autorisations_descr"]:
infos[ descr_autorisations = (
"situation" f"Autorisé à s'inscrire en {pv['autorisations_descr']}."
] += f" Autorisé à s'inscrire en {pv['autorisations_descr']}." )
else: else:
infos["situation"] += " Diplôme obtenu." descr_dec += " Diplôme obtenu."
_format_situation_fields(
infos,
[
"descr_inscription",
"descr_defaillance",
"descr_decisions_ue",
"descr_decision_annee",
],
[descr_dec, descr_mention, descr_autorisations],
)
return infos, dpv return infos, dpv
def _format_situation_fields(
infos, field_names: list[str], extra_values: list[str]
) -> None:
"""Réuni les champs pour former le paragraphe "situation", et ajoute la pontuation aux champs."""
infos["situation"] = ". ".join(
x
for x in [infos.get(field_name, "") for field_name in field_names]
+ [field for field in extra_values if field]
if x
)
for field_name in field_names:
field = infos.get(field_name, "")
if field and not field.endswith("."):
infos[field_name] = "."
# ------ Page bulletin # ------ Page bulletin
def formsemestre_bulletinetud( def formsemestre_bulletinetud(
etud: Identite = None, etud: Identite = None,

View File

@ -228,7 +228,7 @@ class TableRecapWithEvalsCache(ScoDocCache):
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False) def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
formsemestre_id=None, pdfonly=False formsemestre_id=None, pdfonly=False
): ):
"""expire cache pour un semestre (ou tous si formsemestre_id non spécifié). """expire cache pour un semestre (ou tous ceux du département si formsemestre_id non spécifié).
Si pdfonly, n'expire que les bulletins pdf cachés. Si pdfonly, n'expire que les bulletins pdf cachés.
""" """
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre

View File

@ -42,7 +42,7 @@ from app.models import ScolarNews
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError from app.scodoc.sco_exceptions import ScoValueError, ScoNonEmptyFormationObject
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
@ -100,26 +100,38 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
return "\n".join(H) return "\n".join(H)
def do_formation_delete(oid): def do_formation_delete(formation_id):
"""delete a formation (and all its UE, matieres, modules) """delete a formation (and all its UE, matieres, modules)
XXX delete all ues, will break if there are validations ! USE WITH CARE ! Warning: delete all ues, will ask if there are validations !
""" """
F = sco_formations.formation_list(args={"formation_id": oid})[0] formation: Formation = Formation.query.get(formation_id)
if sco_formations.formation_has_locked_sems(oid): if formation is None:
raise ScoLockedFormError() return
cnx = ndb.GetDBConnexion() acronyme = formation.acronyme
# delete all UE in this formation if formation.formsemestres.count():
ues = sco_edit_ue.ue_list({"formation_id": oid}) raise ScoNonEmptyFormationObject(
for ue in ues: type_objet="formation",
sco_edit_ue.do_ue_delete(ue["ue_id"], force=True) msg=formation.titre,
dest_url=url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
),
)
sco_formations._formationEditor.delete(cnx, oid) # Suppression des modules
for module in formation.modules:
db.session.delete(module)
db.session.flush()
# Suppression des UEs
for ue in formation.ues:
sco_edit_ue.do_ue_delete(ue, force=True)
db.session.delete(formation)
# news # news
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=oid, obj=formation_id,
text=f"Suppression de la formation {F['acronyme']}", text=f"Suppression de la formation {acronyme}",
) )

View File

@ -37,7 +37,14 @@ from app import db
from app import log from app import log
from app.but import apc_edit_ue from app.but import apc_edit_ue
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import Formation, UniteEns, ModuleImpl, Module from app.models import (
Formation,
FormSemestreUEComputationExpr,
FormSemestreUECoef,
Matiere,
UniteEns,
)
from app.models import ApcValidationRCUE, ScolarFormSemestreValidation, ScolarEvent
from app.models import ScolarNews from app.models import ScolarNews
from app.models.formations import Matiere from app.models.formations import Matiere
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -79,6 +86,7 @@ _ueEditor = ndb.EditableTable(
"coefficient", "coefficient",
"coef_rcue", "coef_rcue",
"color", "color",
"niveau_competence_id",
), ),
sortkey="numero", sortkey="numero",
input_formators={ input_formators={
@ -138,12 +146,11 @@ def do_ue_create(args):
return ue_id return ue_id
def do_ue_delete(ue_id, delete_validations=False, force=False): def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
"delete UE and attached matieres (but not modules)" """delete UE and attached matieres (but not modules).
from app.scodoc import sco_cursus_dut Si force, pas de confirmation dialog et pas de redirect
"""
ue = UniteEns.query.get_or_404(ue_id) formation: Formation = ue.formation
formation = ue.formation
semestre_idx = ue.semestre_idx semestre_idx = ue.semestre_idx
if not ue.can_be_deleted(): if not ue.can_be_deleted():
raise ScoNonEmptyFormationObject( raise ScoNonEmptyFormationObject(
@ -157,20 +164,22 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
), ),
) )
cnx = ndb.GetDBConnexion() log(f"do_ue_delete: ue_id={ue.id}, delete_validations={delete_validations}")
log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue.id, delete_validations))
# check
# if ue_is_locked(ue.id):
# raise ScoLockedFormError()
# Il y a-t-il des etudiants ayant validé cette UE ? # Il y a-t-il des etudiants ayant validé cette UE ?
# si oui, propose de supprimer les validations # si oui, propose de supprimer les validations
validations = sco_cursus_dut.scolar_formsemestre_validation_list( validations_ue = ScolarFormSemestreValidation.query.filter_by(ue_id=ue.id).all()
cnx, args={"ue_id": ue.id} validations_rcue = ApcValidationRCUE.query.filter(
) (ApcValidationRCUE.ue1_id == ue.id) | (ApcValidationRCUE.ue2_id == ue.id)
if validations and not delete_validations and not force: ).all()
if (
(len(validations_ue) > 0 or len(validations_rcue) > 0)
and not delete_validations
and not force
):
return scu.confirm_dialog( return scu.confirm_dialog(
"<p>%d étudiants ont validé l'UE %s (%s)</p><p>Si vous supprimez cette UE, ces validations vont être supprimées !</p>" f"""<p>Des étudiants ont une décision de jury sur l'UE {ue.acronyme} ({ue.titre})</p>
% (len(validations), ue.acronyme, ue.titre), <p>Si vous supprimez cette UE, ces décisions vont être supprimées !</p>""",
dest_url="", dest_url="",
target_variable="delete_validations", target_variable="delete_validations",
cancel_url=url_for( cancel_url=url_for(
@ -183,31 +192,34 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
) )
if delete_validations: if delete_validations:
log(f"deleting all validations of UE {ue.id}") log(f"deleting all validations of UE {ue.id}")
ndb.SimpleQuery( for v in validations_ue:
"DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", db.session.delete(v)
{"ue_id": ue.id}, for v in validations_rcue:
) db.session.delete(v)
# delete old formulas # delete old formulas
ndb.SimpleQuery( formulas = FormSemestreUEComputationExpr.query.filter_by(ue_id=ue.id).all()
"DELETE FROM notes_formsemestre_ue_computation_expr WHERE ue_id=%(ue_id)s", for formula in formulas:
{"ue_id": ue.id}, db.session.delete(formula)
)
# delete all matiere in this UE # delete all matieres in this UE
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id}) for mat in Matiere.query.filter_by(ue_id=ue.id):
for mat in mats: db.session.delete(mat)
sco_edit_matiere.do_matiere_delete(mat["matiere_id"])
# delete uecoef and events # delete uecoefs
ndb.SimpleQuery( for uecoef in FormSemestreUECoef.query.filter_by(ue_id=ue.id):
"DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s", db.session.delete(uecoef)
{"ue_id": ue.id}, # delete events
) for event in ScolarEvent.query.filter_by(ue_id=ue.id):
ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue.id}) db.session.delete(event)
cnx = ndb.GetDBConnexion() db.session.flush()
_ueEditor.delete(cnx, ue.id)
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement db.session.delete(ue)
# utilisé: acceptable de tout invalider): db.session.commit()
# cas compliqué, mais rarement utilisé: acceptable de tout invalider
formation.invalidate_module_coefs() formation.invalidate_module_coefs()
# -> invalide aussi .invalidate_formsemestre() # -> invalide aussi les formsemestres
# news # news
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
@ -601,7 +613,7 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
), ),
) )
return do_ue_delete(ue.id, delete_validations=delete_validations) return do_ue_delete(ue, delete_validations=delete_validations)
def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list

View File

@ -658,8 +658,10 @@ def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
info = sco_etape_apogee.apo_csv_get_archive( info = sco_etape_apogee.apo_csv_get_archive(
etape_apo, semset["annee_scolaire"], semset["sem_id"] etape_apo, semset["annee_scolaire"], semset["sem_id"]
) )
if info:
sco_etape_apogee.apo_csv_delete(info["archive_id"]) sco_etape_apogee.apo_csv_delete(info["archive_id"])
return flask.redirect(dest_url + "&head_message=Archive%20supprimée") return flask.redirect(dest_url + "&head_message=Archive%20supprimée")
return flask.redirect(dest_url + "&head_message=Archive%20inexistante")
def view_apo_csv(etape_apo="", semset_id="", format="html"): def view_apo_csv(etape_apo="", semset_id="", format="html"):

View File

@ -116,7 +116,7 @@ class ScoNonEmptyFormationObject(ScoValueError):
"""On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent""" """On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent"""
def __init__(self, type_objet="objet'", msg="", dest_url=None): def __init__(self, type_objet="objet'", msg="", dest_url=None):
msg = f"""<h3>{type_objet} "{msg}" utilisé dans des semestres: suppression impossible.</h3> msg = f"""<h3>{type_objet} "{msg}" utilisé(e) dans des semestres: suppression impossible.</h3>
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce {type_objet}). <p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce {type_objet}).
Mais il est peut-être préférable de laisser ce programme intact et d'en créer une Mais il est peut-être préférable de laisser ce programme intact et d'en créer une
nouvelle version pour la modifier sans affecter les semestres déjà en place. nouvelle version pour la modifier sans affecter les semestres déjà en place.

View File

@ -31,7 +31,7 @@ from operator import itemgetter
import xml.dom.minidom import xml.dom.minidom
import flask import flask
from flask import g, url_for, request from flask import flash, g, url_for, request
from flask_login import current_user from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -65,6 +65,7 @@ _formationEditor = ndb.EditableTable(
"formation_code", "formation_code",
"type_parcours", "type_parcours",
"code_specialite", "code_specialite",
"referentiel_competence_id",
), ),
filter_dept=True, filter_dept=True,
sortkey="acronyme", sortkey="acronyme",
@ -104,7 +105,7 @@ def formation_export(
"""Get a formation, with UE, matieres, modules """Get a formation, with UE, matieres, modules
in desired format in desired format
""" """
formation = Formation.query.get_or_404(formation_id) formation: Formation = Formation.query.get_or_404(formation_id)
F = formation.to_dict() F = formation.to_dict()
selector = {"formation_id": formation_id} selector = {"formation_id": formation_id}
if not export_external_ues: if not export_external_ues:
@ -424,17 +425,18 @@ def formation_list_table(formation_id=None, args={}):
def formation_create_new_version(formation_id, redirect=True): def formation_create_new_version(formation_id, redirect=True):
"duplicate formation, with new version number" "duplicate formation, with new version number"
formation = Formation.query.get_or_404(formation_id)
resp = formation_export(formation_id, export_ids=True, format="xml") resp = formation_export(formation_id, export_ids=True, format="xml")
xml_data = resp.get_data(as_text=True) xml_data = resp.get_data(as_text=True)
new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data) new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data)
# news # news
F = formation_list(args={"formation_id": new_id})[0]
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=new_id, obj=new_id,
text="Nouvelle version de la formation %(acronyme)s" % F, text=f"Nouvelle version de la formation {formation.acronyme}",
) )
if redirect: if redirect:
flash("Nouvelle version !")
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_table", "notes.ue_table",

View File

@ -746,7 +746,7 @@ def do_formsemestre_createwithmodules(edit=False):
if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]): if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]):
msg = '<ul class="tf-msg"><li class="tf-msg">Dates de début et fin incompatibles !</li></ul>' msg = '<ul class="tf-msg"><li class="tf-msg">Dates de début et fin incompatibles !</li></ul>'
if sco_preferences.get_preference("always_require_apo_sem_codes") and not any( if sco_preferences.get_preference("always_require_apo_sem_codes") and not any(
[tf[2]["etape_apo" + str(n)] for n in range(0, scu.EDIT_NB_ETAPES + 1)] [tf[2]["etape_apo" + str(n)] for n in range(1, scu.EDIT_NB_ETAPES + 1)]
): ):
msg = '<ul class="tf-msg"><li class="tf-msg">Code étape Apogée manquant</li></ul>' msg = '<ul class="tf-msg"><li class="tf-msg">Code étape Apogée manquant</li></ul>'

View File

@ -679,18 +679,12 @@ def formsemestre_recap_parcours_table(
class_ue = "ue" class_ue = "ue"
if ue_status and ue_status["is_external"]: # validation externe if ue_status and ue_status["is_external"]: # validation externe
explanation_ue.append("UE externe.") explanation_ue.append("UE externe.")
# log('x'*12+' EXTERNAL %s' % notes_table.fmt_note(moy_ue)) XXXXXXX
# log('UE=%s' % pprint.pformat(ue))
# log('explanation_ue=%s\n'%explanation_ue)
if ue_status and ue_status["is_capitalized"]: if ue_status and ue_status["is_capitalized"]:
class_ue += " ue_capitalized" class_ue += " ue_capitalized"
explanation_ue.append( explanation_ue.append(
"Capitalisée le %s." % (ue_status["event_date"] or "?") "Capitalisée le %s." % (ue_status["event_date"] or "?")
) )
# log('x'*12+' CAPITALIZED %s' % notes_table.fmt_note(moy_ue))
# log('UE=%s' % pprint.pformat(ue))
# log('UE_STATUS=%s' % pprint.pformat(ue_status)) XXXXXX
# log('')
H.append( H.append(
'<td class="%s" title="%s">%s</td>' '<td class="%s" title="%s">%s</td>'
@ -712,27 +706,30 @@ def formsemestre_recap_parcours_table(
sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"]) sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"])
or nt.parcours.ECTS_ONLY or nt.parcours.ECTS_ONLY
): ):
etud_ects_infos = nt.get_etud_ects_pot(etudid) etud_ects_infos = nt.get_etud_ects_pot(etudid) # ECTS potentiels
H.append( H.append(
'<tr class="%s rcp_l2 sem_%s">' % (class_sem, sem["formsemestre_id"]) f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">
<td class="rcp_type_sem" style="background-color:{bgcolor};">&nbsp;</td>
<td></td>"""
) )
# Total ECTS (affiché sous la moyenne générale)
H.append( H.append(
'<td class="rcp_type_sem" style="background-color:%s;">&nbsp;</td><td></td>' f"""<td class="sem_ects_tit"><a title="crédit acquis">ECTS:</a></td>
% (bgcolor) <td class="sem_ects">{pv.get("sum_ects",0):2.2g} / {etud_ects_infos["ects_total"]:2.2g}</td>
<td class="rcp_abs"></td>
"""
) )
# total ECTS (affiché sous la moyenne générale)
H.append(
'<td class="sem_ects_tit"><a title="crédit potentiels">ECTS:</a></td><td class="sem_ects">%g</td>'
% (etud_ects_infos["ects_pot"])
)
H.append('<td class="rcp_abs"></td>')
# ECTS validables dans chaque UE # ECTS validables dans chaque UE
for ue in ues: for ue in ues:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if ue_status:
ects = ue_status["ects"]
ects_pot = ue_status["ects_pot"]
H.append( H.append(
'<td class="ue">%g</td>' f"""<td class="ue" title="{ects:2.2g}/{ects_pot:2.2g} ECTS">{ects:2.2g}</td>"""
% (ue_status["ects_pot"] if ue_status else "")
) )
else:
H.append(f"""<td class="ue"></td>""")
H.append("<td></td></tr>") H.append("<td></td></tr>")
H.append("</table>") H.append("</table>")

View File

@ -136,7 +136,9 @@ def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
if os.access(path_dir.joinpath(entry).absolute(), os.R_OK): if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
result = filename_parser.match(entry.name) result = filename_parser.match(entry.name)
if result: if result:
logoname = result.group(1)[:-1] # retreive logoname from filename (less final dot) logoname = result.group(1)[
:-1
] # retreive logoname from filename (less final dot)
logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select() logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
return logos if len(logos.keys()) > 0 else None return logos if len(logos.keys()) > 0 else None
@ -235,7 +237,7 @@ class Logo:
unit = img.info.get("jfif_unit", 0) # 0 = no unit ; 1 = inch ; 2 = mm unit = img.info.get("jfif_unit", 0) # 0 = no unit ; 1 = inch ; 2 = mm
if self.density is not None: if self.density is not None:
x_density, y_density = self.density x_density, y_density = self.density
if unit != 0: if unit != 0 and x_density != 0 and y_density != 0:
unit2mm = [0, 1 / 0.254, 0.1][unit] unit2mm = [0, 1 / 0.254, 0.1][unit]
x_mm = round(x_size * unit2mm / x_density, 2) x_mm = round(x_size * unit2mm / x_density, 2)
y_mm = round(y_size * unit2mm / y_density, 2) y_mm = round(y_size * unit2mm / y_density, 2)
@ -244,7 +246,6 @@ class Logo:
self.mm = None self.mm = None
else: else:
self.mm = None self.mm = None
self.size = (x_size, y_size) self.size = (x_size, y_size)
self.aspect_ratio = round(float(x_size) / y_size, 2) self.aspect_ratio = round(float(x_size) / y_size, 2)

View File

@ -206,6 +206,7 @@ def dict_pvjury(
{ {
'date' : date de la decision la plus recente, 'date' : date de la decision la plus recente,
'formsemestre' : sem, 'formsemestre' : sem,
'is_apc' : bool,
'formation' : { 'acronyme' :, 'titre': ... } 'formation' : { 'acronyme' :, 'titre': ... }
'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,}, 'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,},
'etat' : I ou D ou DEF 'etat' : I ou D ou DEF
@ -359,6 +360,7 @@ def dict_pvjury(
return { return {
"date": ndb.DateISOtoDMY(max_date), "date": ndb.DateISOtoDMY(max_date),
"formsemestre": sem, "formsemestre": sem,
"is_apc": nt.is_apc,
"has_prev": has_prev, "has_prev": has_prev,
"semestre_non_terminal": semestre_non_terminal, "semestre_non_terminal": semestre_non_terminal,
"formation": sco_formations.formation_list( "formation": sco_formations.formation_list(
@ -418,7 +420,10 @@ def pvjury_table(
titles["prev_decision"] = "Décision S%s" % id_prev titles["prev_decision"] = "Décision S%s" % id_prev
columns_ids += ["prev_decision"] columns_ids += ["prev_decision"]
if not dpv["is_apc"]:
# Décision de jury sur le semestre, sauf en BUT
columns_ids += ["decision"] columns_ids += ["decision"]
if sco_preferences.get_preference("bul_show_mention", formsemestre_id): if sco_preferences.get_preference("bul_show_mention", formsemestre_id):
columns_ids += ["mention"] columns_ids += ["mention"]
columns_ids += ["ue_cap"] columns_ids += ["ue_cap"]
@ -444,7 +449,7 @@ def pvjury_table(
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=e["identite"]["etudid"], etudid=e["identite"]["etudid"],
), ),
"_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % e["identite"]["etudid"], "_nomprenom_td_attrs": f"""id="{e['identite']['etudid']}" class="etudinfo" """,
"parcours": e["parcours"], "parcours": e["parcours"],
"decision": _descr_decision_sem_abbrev(e["etat"], e["decision_sem"]), "decision": _descr_decision_sem_abbrev(e["etat"], e["decision_sem"]),
"ue_cap": e["decisions_ue_descr"], "ue_cap": e["decisions_ue_descr"],
@ -508,7 +513,8 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
# Bretelle provisoire pour BUT 9.3.0 # Bretelle provisoire pour BUT 9.3.0
# XXX TODO # XXX TODO
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0: is_apc = formsemestre.formation.is_apc()
if format == "html" and is_apc and formsemestre.semestre_id % 2 == 0:
from app.but import jury_but_recap from app.but import jury_but_recap
return jury_but_recap.formsemestre_saisie_jury_but( return jury_but_recap.formsemestre_saisie_jury_but(

View File

@ -2,57 +2,75 @@
/*******************/ /*******************/
/* Styles généraux */ /* Styles généraux */
/*******************/ /*******************/
.wait{ .wait {
width: 60px; width: 60px;
height: 6px; height: 6px;
margin: auto; margin: auto;
background: #424242; /* la réponse à tout */ background: #424242;
/* la réponse à tout */
animation: wait .4s infinite alternate; animation: wait .4s infinite alternate;
} }
@keyframes wait{
100%{transform: translateY(40px) rotate(1turn);} @keyframes wait {
100% {
transform: translateY(40px) rotate(1turn);
}
} }
main{
--couleurPrincipale: rgb(240,250,255); main {
--couleurPrincipale: rgb(240, 250, 255);
--couleurFondTitresUE: #b6ebff; --couleurFondTitresUE: #b6ebff;
--couleurFondTitresRes: #f8c844; --couleurFondTitresRes: #f8c844;
--couleurFondTitresSAE: #c6ffab; --couleurFondTitresSAE: #c6ffab;
--couleurSecondaire: #fec; --couleurSecondaire: #fec;
--couleurIntense: rgb(4, 16, 159);; --couleurIntense: rgb(4, 16, 159);
;
--couleurSurlignage: rgba(255, 253, 110, 0.49); --couleurSurlignage: rgba(255, 253, 110, 0.49);
max-width: 1000px; max-width: 1000px;
margin: auto; margin: auto;
display: none; display: none;
} }
.releve a, .releve a:visited {
.releve a,
.releve a:visited {
color: navy; color: navy;
text-decoration: none; text-decoration: none;
} }
.releve a:hover { .releve a:hover {
color: red; color: red;
text-decoration: underline; text-decoration: underline;
} }
.ready .wait{display: none;} .ready .wait {
.ready main{display: block;} display: none;
h2{ }
.ready main {
display: block;
}
h2 {
margin: 0; margin: 0;
color: black; color: black;
} }
section{
section {
background: #FFF; background: #FFF;
border-radius: 16px; border-radius: 16px;
border: 1px solid #AAA; border: 1px solid #AAA;
padding: 16px 32px; padding: 16px 32px;
margin: 8px 0; margin: 8px 0;
} }
section>div:nth-child(1){
section>div:nth-child(1) {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.CTA_Liste{
.CTA_Liste {
display: flex; display: flex;
gap: 4px; gap: 4px;
align-items: center; align-items: center;
@ -60,40 +78,46 @@ section>div:nth-child(1){
color: #FFF; color: #FFF;
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 2px 2px rgba(0,0,0,0.26); box-shadow: 0 2px 2px rgba(0, 0, 0, 0.26);
cursor: pointer; cursor: pointer;
} }
.CTA_Liste>svg{
.CTA_Liste>svg {
transition: 0.2s; transition: 0.2s;
} }
.CTA_Liste:hover{
.CTA_Liste:hover {
outline: 2px solid #424242; outline: 2px solid #424242;
} }
.listeOff svg{
.listeOff svg {
transform: rotate(180deg); transform: rotate(180deg);
} }
.listeOff .syntheseModule, .listeOff .syntheseModule,
.listeOff .eval{ .listeOff .eval {
display: none; display: none;
} }
.moduleOnOff>.syntheseModule, .moduleOnOff>.syntheseModule,
.moduleOnOff>.eval{ .moduleOnOff>.eval {
display: none; display: none;
} }
.listeOff .moduleOnOff>.syntheseModule, .listeOff .moduleOnOff>.syntheseModule,
.listeOff .moduleOnOff>.eval{ .listeOff .moduleOnOff>.eval {
display: flex !important; display: flex !important;
} }
.listeOff .ue::before, .listeOff .ue::before,
.listeOff .module::before, .listeOff .module::before,
.moduleOnOff .ue::before, .moduleOnOff .ue::before,
.moduleOnOff .module::before{ .moduleOnOff .module::before {
transform: rotate(0); transform: rotate(0);
} }
.listeOff .moduleOnOff .ue::before, .listeOff .moduleOnOff .ue::before,
.listeOff .moduleOnOff .module::before{ .listeOff .moduleOnOff .module::before {
transform: rotate(180deg) !important; transform: rotate(180deg) !important;
} }
@ -107,24 +131,25 @@ section>div:nth-child(1){
.hide_coef .eval>em, .hide_coef .eval>em,
.hide_date_inscr .dateInscription, .hide_date_inscr .dateInscription,
.hide_ects .ects, .hide_ects .ects,
.hide_rangs .rang{ .hide_rangs .rang {
display: none; display: none;
} }
/*.module>.absences,*/ /*.module>.absences,*/
.module .moyenne, .module .moyenne,
.module .info{ .module .info {
display: none; display: none;
} }
/************/ /************/
/* Etudiant */ /* Etudiant */
/************/ /************/
.info_etudiant{ .info_etudiant {
color: #000; color: #000;
text-decoration: none; text-decoration: none;
} }
.etudiant{
.etudiant {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
@ -132,7 +157,8 @@ section>div:nth-child(1){
background: var(--couleurPrincipale); background: var(--couleurPrincipale);
color: rgb(0, 0, 0); color: rgb(0, 0, 0);
} }
.civilite{
.civilite {
font-weight: bold; font-weight: bold;
font-size: 130%; font-size: 130%;
} }
@ -140,19 +166,21 @@ section>div:nth-child(1){
/************/ /************/
/* Semestre */ /* Semestre */
/************/ /************/
.flex{ .flex {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
margin-top: 8px; margin-top: 8px;
} }
.infoSemestre{
.infoSemestre {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
gap: 4px; gap: 4px;
} }
.infoSemestre>div{
.infoSemestre>div {
border: 1px solid var(--couleurIntense); border: 1px solid var(--couleurIntense);
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
@ -162,48 +190,61 @@ section>div:nth-child(1){
flex: none; flex: none;
} }
.infoSemestre>div>div:nth-child(even){ .infoSemestre>div>div:nth-child(even) {
text-align: right; text-align: right;
} }
.photo { .photo {
border: none; border: none;
margin-left: auto; margin-left: auto;
} }
.rang, .competence{
.rang,
.competence {
font-weight: bold; font-weight: bold;
} }
.ue .rang{
.ue .rang {
font-weight: 400; font-weight: 400;
} }
.absencesRecap { .absencesRecap {
align-items: baseline; align-items: baseline;
} }
.absencesRecap > div:nth-child(2n) {
.absencesRecap>div:nth-child(2n) {
font-weight: normal; font-weight: normal;
} }
.abs { .abs {
font-weight: bold; font-weight: bold;
} }
.decision{
.decision {
margin: 5px 0; margin: 5px 0;
font-weight: bold; font-weight: bold;
font-size: 20px; font-size: 16px;
} }
#ects_tot, .decision, .decision_annee {
#ects_tot,
.decision,
.decision_annee {
font-weight: bold; font-weight: bold;
font-size: 20px; font-size: 16px;
margin-top: 8px; margin-top: 8px;
} }
.enteteSemestre{
.enteteSemestre {
color: black; color: black;
font-weight: bold; font-weight: bold;
font-size: 20px; font-size: 16px;
margin-bottom: 4px; margin-bottom: 4px;
} }
/***************/ /***************/
/* Zone custom */ /* Zone custom */
/***************/ /***************/
.custom:empty{ .custom:empty {
display: none; display: none;
} }
@ -211,20 +252,23 @@ section>div:nth-child(1){
/* Synthèse */ /* Synthèse */
/***************/ /***************/
.synthese .ue, .synthese .ue,
.synthese h3{ .synthese h3 {
background: var(--couleurFondTitresUE); background: var(--couleurFondTitresUE);
} }
.synthese .ue>div{
.synthese .ue>div {
text-align: right; text-align: right;
} }
.synthese em, .synthese em,
.eval em{ .eval em {
opacity: 0.6; opacity: 0.6;
min-width: 80px; min-width: 80px;
display: inline-block; display: inline-block;
} }
.ueBonus, .ueBonus,
.ueBonus h3{ .ueBonus h3 {
background: var(--couleurFondTitresSAE) !important; background: var(--couleurFondTitresSAE) !important;
color: #000 !important; color: #000 !important;
} }
@ -233,10 +277,12 @@ section>div:nth-child(1){
/* Evaluations */ /* Evaluations */
/***************/ /***************/
.evaluations>div, .evaluations>div,
.sae>div{ .sae>div {
scroll-margin-top: 60px; scroll-margin-top: 60px;
} }
.module, .ue {
.module,
.ue {
color: #000; color: #000;
padding: 4px 32px; padding: 4px 32px;
border-radius: 4px; border-radius: 4px;
@ -248,17 +294,22 @@ section>div:nth-child(1){
cursor: pointer; cursor: pointer;
position: relative; position: relative;
} }
.ue { .ue {
background: var(--couleurFondTitresRes); background: var(--couleurFondTitresRes);
} }
.module { .module {
background: var(--couleurFondTitresRes); background: var(--couleurFondTitresRes);
} }
.module h3 { .module h3 {
background: var(--couleurFondTitresRes); background: var(--couleurFondTitresRes);
} }
.module::before, .ue::before {
content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26px' height='26px' fill='white'><path d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /></svg>"); .module::before,
.ue::before {
content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26px' height='26px' fill='white'><path d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /></svg>");
width: 26px; width: 26px;
height: 26px; height: 26px;
position: absolute; position: absolute;
@ -267,14 +318,18 @@ section>div:nth-child(1){
transform: rotate(180deg); transform: rotate(180deg);
transition: 0.2s; transition: 0.2s;
} }
@media screen and (max-width: 1000px) { @media screen and (max-width: 1000px) {
/* Placer le chevron à gauche au lieu du milieu */ /* Placer le chevron à gauche au lieu du milieu */
.module::before, .ue::before { .module::before,
.ue::before {
left: 2px; left: 2px;
bottom: calc(50% - 13px); bottom: calc(50% - 13px);
} }
} }
h3{
h3 {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0 auto 0 0; margin: 0 auto 0 0;
@ -284,21 +339,27 @@ h3{
font-size: 16px; font-size: 16px;
background: var(--couleurSecondaire); background: var(--couleurSecondaire);
} }
.sae .module, .sae h3{
.sae .module,
.sae h3 {
background: var(--couleurFondTitresSAE); background: var(--couleurFondTitresSAE);
} }
.moyenne{ .moyenne {
font-weight: bold; font-weight: bold;
text-align: right; text-align: right;
} }
.info{
.info {
opacity: 0.9; opacity: 0.9;
} }
.syntheseModule{
.syntheseModule {
cursor: pointer; cursor: pointer;
} }
.eval, .syntheseModule{
.eval,
.syntheseModule {
position: relative; position: relative;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -306,17 +367,21 @@ h3{
padding: 0px 4px; padding: 0px 4px;
border-bottom: 1px solid #aaa; border-bottom: 1px solid #aaa;
} }
.eval>div, .syntheseModule>div{
.eval>div,
.syntheseModule>div {
display: flex; display: flex;
gap: 4px; gap: 4px;
} }
.eval:hover, .syntheseModule:hover{ .eval:hover,
.syntheseModule:hover {
background: var(--couleurSurlignage); background: var(--couleurSurlignage);
/* color: #FFF; */ /* color: #FFF; */
} }
.complement{
pointer-events:none; .complement {
pointer-events: none;
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
right: 0; right: 0;
@ -331,30 +396,37 @@ h3{
gap: 0 !important; gap: 0 !important;
column-gap: 4px !important; column-gap: 4px !important;
} }
.eval:hover .complement{
.eval:hover .complement {
opacity: 1; opacity: 1;
z-index: 1; z-index: 1;
} }
.complement>div:nth-child(even){
.complement>div:nth-child(even) {
text-align: right; text-align: right;
} }
.complement>div:nth-child(1), .complement>div:nth-child(1),
.complement>div:nth-child(2){ .complement>div:nth-child(2) {
font-weight: bold; font-weight: bold;
} }
.complement>div:nth-child(1), .complement>div:nth-child(1),
.complement>div:nth-child(7){ .complement>div:nth-child(7) {
margin-bottom: 8px; margin-bottom: 8px;
} }
@media screen and (max-width: 700px) { @media screen and (max-width: 700px) {
section{ section {
padding: 16px; padding: 16px;
} }
.syntheseModule, .eval {
.syntheseModule,
.eval {
margin: 0; margin: 0;
} }
} }
/*.absences{ /*.absences{
display: grid; display: grid;
grid-template-columns: auto auto; grid-template-columns: auto auto;

View File

@ -46,9 +46,29 @@ function set_ue_niveau_competence(elem) {
niveau_id: niveau_id, niveau_id: niveau_id,
}, },
function (result) { function (result) {
alert("niveau de compétence enregistré"); // XXX #frontend à améliorer // alert("niveau de compétence enregistré"); // XXX #frontend à améliorer
// obj.classList.remove("sco_wait"); sco_message("niveau de compétence enregistré");
// obj.classList.add("sco_modified");
update_menus_niveau_competence();
}
);
}
// Met à jour les niveaux utilisés (disabled) ou non affectés
// dans les menus d'association UE <-> niveau
function update_menus_niveau_competence() {
let selected_niveaux = [];
document.querySelectorAll("form.form_ue_choix_niveau select").forEach(
elem => { selected_niveaux.push(elem.value); }
);
document.querySelectorAll("form.form_ue_choix_niveau select").forEach(
elem => {
for (let i = 0; i < elem.options.length; i++) {
elem.options[i].disabled = (i != elem.options.selectedIndex)
&& (selected_niveaux.indexOf(elem.options[i].value) != -1)
&& (elem.options[i].value != "");
}
} }
); );
} }

View File

@ -85,7 +85,7 @@ function sco_message(msg, color) {
} }
); );
}, },
2000 // <-- duree affichage en milliseconds 3000 // <-- duree affichage en milliseconds
); );
} }

View File

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

View File

@ -327,7 +327,7 @@ def test_formations(test_client):
# --- Suppression d'une formation # --- Suppression d'une formation
sco_edit_formation.do_formation_delete(oid=formation_id2) sco_edit_formation.do_formation_delete(formation_id=formation_id2)
lif3 = notes.formation_list(format="json").get_data(as_text=True) lif3 = notes.formation_list(format="json").get_data(as_text=True)
assert isinstance(lif3, str) assert isinstance(lif3, str)
load_lif3 = json.loads(lif3) load_lif3 = json.loads(lif3)