forked from ScoDoc/ScoDoc
Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into entreprises
This commit is contained in:
commit
3a7879c8aa
@ -68,7 +68,7 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
||||
options_str = "\n".join(options)
|
||||
return f"""
|
||||
<div class="ue_choix_niveau">
|
||||
<form id="form_ue_choix_niveau">
|
||||
<form class="form_ue_choix_niveau">
|
||||
<b>Niveau de compétence associé:</b>
|
||||
<select onchange="set_ue_niveau_competence(this);"
|
||||
data-ue_id="{ue.id}"
|
||||
|
@ -321,8 +321,8 @@ class BulletinBUT:
|
||||
}
|
||||
decisions_ues = self.res.get_etud_decision_ues(etud.id) or {}
|
||||
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_acquis = sum([d.get("ects", 0) for d in decisions_ues.values()])
|
||||
ects_tot = res.etud_ects_tot_sem(etud.id)
|
||||
ects_acquis = res.get_etud_ects_valides(etud.id, decisions_ues)
|
||||
semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
|
||||
if sco_preferences.get_preference("bul_show_decision", formsemestre.id):
|
||||
semestre_infos.update(
|
||||
|
@ -160,6 +160,9 @@ class DecisionsProposees:
|
||||
"Validation enregistrée"
|
||||
self.code_valide: str = code_valide
|
||||
"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
|
||||
"Explication à afficher à côté de la décision"
|
||||
self.recorded = False
|
||||
@ -784,6 +787,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
self.code_valide = self.validation.code
|
||||
if rcue.est_compensable():
|
||||
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():
|
||||
self.codes.insert(0, sco_codes.ADM)
|
||||
else:
|
||||
@ -885,11 +890,21 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
ue: UniteEns,
|
||||
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.ue: UniteEns = ue
|
||||
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
|
||||
"inscription: I, DEM, DEF"
|
||||
if ue.type == sco_codes.UE_SPORT:
|
||||
@ -904,13 +919,6 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
]
|
||||
self.moy_ue = "-"
|
||||
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 ?
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
@ -38,7 +38,7 @@ def _descr_cursus_but(etud: Identite) -> str:
|
||||
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
|
||||
formsemestre peut être pair ou impair
|
||||
"""
|
||||
|
@ -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_2.id])
|
||||
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
|
||||
row.add_cell(
|
||||
"code_annee",
|
||||
|
@ -607,6 +607,28 @@ class BonusCachan1(BonusSportAdditif):
|
||||
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):
|
||||
"""Calcul bonus modules optionnels (sport, culture), règle IUT LCO.
|
||||
|
||||
@ -955,20 +977,53 @@ class BonusLimousin(BonusSportAdditif):
|
||||
bonus_max = 0.5
|
||||
|
||||
|
||||
class BonusLyonProvisoire(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionnels (sport, culture) à l'IUT de Lyon (provisoire)
|
||||
class BonusLyon(BonusSportAdditif):
|
||||
"""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
|
||||
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"
|
||||
displayed_name = "IUT de Lyon (provisoire)"
|
||||
displayed_name = "IUT de Lyon"
|
||||
seuil_moy_gen = 10.0 # points comptés au dessus de 10.
|
||||
proportion_point = 0.018
|
||||
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):
|
||||
"""Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines.
|
||||
|
@ -32,7 +32,7 @@ class ValidationsSemestre(ResultatsCache):
|
||||
{ etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}"""
|
||||
self.decisions_jury_ues = {}
|
||||
"""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
|
||||
"""DataFrame, index etudid
|
||||
@ -55,7 +55,9 @@ class ValidationsSemestre(ResultatsCache):
|
||||
"""Cherche les decisions du jury pour le semestre (pas les UE).
|
||||
Calcule les attributs:
|
||||
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 l'étudiant est défaillant, pas de décisions d'UE.
|
||||
"""
|
||||
|
@ -6,7 +6,6 @@
|
||||
|
||||
"""Résultats semestres BUT
|
||||
"""
|
||||
from collections.abc import Generator
|
||||
from re import U
|
||||
import time
|
||||
import numpy as np
|
||||
@ -235,7 +234,3 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
"""
|
||||
s = self.ues_inscr_parcours_df.loc[etudid]
|
||||
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))
|
||||
|
@ -8,6 +8,7 @@
|
||||
"""
|
||||
|
||||
from collections import Counter
|
||||
from collections.abc import Generator
|
||||
from functools import cached_property
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@ -120,6 +121,15 @@ class ResultatsSemestre(ResultatsCache):
|
||||
# car tous les étudiants sont inscrits à toutes les UE
|
||||
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:
|
||||
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||
Utile pour stats bottom tableau recap.
|
||||
@ -167,7 +177,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
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
|
||||
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
|
||||
susceptibles d'être validées.
|
||||
@ -175,9 +185,8 @@ class ResultatsSemestre(ResultatsCache):
|
||||
Les UE "bonus" (sport) ne sont jamais "validables".
|
||||
"""
|
||||
if self.is_apc:
|
||||
# TODO: introduire la notion de parcours (#sco93)
|
||||
return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
|
||||
else:
|
||||
return list(self.etud_ues(etudid))
|
||||
# Formations classiques:
|
||||
# restreint aux UE auxquelles l'étudiant est inscrit (dans l'un des modimpls)
|
||||
ues = {
|
||||
modimpl.module.ue
|
||||
@ -192,7 +201,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
Utile en formations classiques, surchargée pour le BUT.
|
||||
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
|
||||
modimpls = [
|
||||
modimpl
|
||||
@ -315,6 +324,8 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"formsemestre_id": None,
|
||||
"capitalized_ue_id": None,
|
||||
"ects_pot": 0.0,
|
||||
"ects": 0.0,
|
||||
"ects_ue": ue.ects,
|
||||
}
|
||||
if not ue_id in self.etud_moy_ue:
|
||||
return None
|
||||
@ -369,6 +380,12 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
|
||||
"coef_ue": coef_ue,
|
||||
"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,
|
||||
"moy": moy_ue,
|
||||
"event_date": ue_cap["event_date"] if is_capitalized else None,
|
||||
|
@ -278,7 +278,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
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.
|
||||
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
|
||||
"""
|
||||
if self.get_etud_etat(etudid) == DEF:
|
||||
@ -290,6 +290,17 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
)
|
||||
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:
|
||||
"""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 }
|
||||
@ -322,7 +333,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
|
||||
"""La moyenne de l'étudiant dans le moduleimpl
|
||||
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
|
||||
|
||||
@ -340,24 +351,25 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
ects_pot : (float) nb de crédits ECTS qui seraient validés
|
||||
(sous réserve de validation par le jury)
|
||||
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
|
||||
encore enregistrées).
|
||||
Les ects_pot sont les ECTS des UE au dessus de la barre (10/20 en principe),
|
||||
avant le jury (donc non encore enregistrés).
|
||||
"""
|
||||
# was nt.get_etud_moy_infos
|
||||
# XXX pour compat nt, à remplacer ultérieurement
|
||||
ues = self.get_etud_ue_validables(etudid)
|
||||
ects_pot = 0.0
|
||||
ects_total = 0.0
|
||||
for ue in ues:
|
||||
if (
|
||||
ue.id in self.etud_moy_ue
|
||||
and ue.ects is not None
|
||||
and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE
|
||||
):
|
||||
if ue.id in self.etud_moy_ue and ue.ects is not None:
|
||||
ects_total += ue.ects
|
||||
if self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE:
|
||||
ects_pot += ue.ects
|
||||
return {
|
||||
"ects_pot": ects_pot,
|
||||
"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]:
|
||||
|
@ -105,9 +105,11 @@ class Formation(db.Model):
|
||||
return len(self.formsemestres.filter_by(etat=False).all()) > 0
|
||||
|
||||
def invalidate_module_coefs(self, semestre_idx: int = None):
|
||||
"""Invalide les coefficients de modules cachés.
|
||||
Si semestre_idx est None, invalide tous les semestres,
|
||||
"""Invalide le cache des coefficients de modules.
|
||||
Si semestre_idx est None, invalide les coefs de tous les semestres,
|
||||
sinon invalide le semestre indiqué et le cache de la formation.
|
||||
|
||||
Dans tous les cas, invalide tous les formsemestres.
|
||||
"""
|
||||
if semestre_idx is None:
|
||||
keys = {f"{self.id}.{m.semestre_id}" for m in self.modules}
|
||||
|
@ -83,10 +83,9 @@ class UniteEns(db.Model):
|
||||
return sco_edit_ue.ue_is_locked(self.id)
|
||||
|
||||
def can_be_deleted(self) -> bool:
|
||||
"""True si l'UE n'est pas utilisée dans des formsemestre
|
||||
et n'a pas de module rattachés
|
||||
"""True si l'UE n'a pas de moduleimpl 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(
|
||||
m.modimpls.all() for m in self.modules
|
||||
)
|
||||
|
@ -433,8 +433,6 @@ class ApoEtud(dict):
|
||||
return VOID_APO_RES
|
||||
|
||||
# Elements UE
|
||||
# if etudid == 3661 and nt.formsemestre.semestre_id == 2: # XXX XXX XXX
|
||||
# breakpoint()
|
||||
decisions_ue = nt.get_etud_decision_ues(etudid)
|
||||
for ue in nt.get_ues_stat_dict():
|
||||
if ue["code_apogee"] and code in {
|
||||
|
@ -65,6 +65,7 @@ from flask_login import current_user
|
||||
import app.scodoc.sco_utils as scu
|
||||
from config import Config
|
||||
from app import log
|
||||
from app.but import jury_but_pv
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Departement, FormSemestre
|
||||
@ -361,8 +362,14 @@ def do_formsemestre_archive(
|
||||
data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
|
||||
if data:
|
||||
PVArchive.store(archive_id, "Bulletins.json", data_js)
|
||||
# Decisions de jury, en XLS
|
||||
data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False)
|
||||
# Décisions de jury, en XLS
|
||||
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:
|
||||
PVArchive.store(
|
||||
archive_id,
|
||||
@ -385,7 +392,10 @@ def do_formsemestre_archive(
|
||||
)
|
||||
if 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)
|
||||
data = sco_pvpdf.pvjury_pdf(
|
||||
dpv,
|
||||
|
@ -757,15 +757,16 @@ def etud_descr_situation_semestre(
|
||||
if show_date_inscr:
|
||||
if not date_inscr:
|
||||
infos["date_inscription"] = ""
|
||||
infos["descr_inscription"] = f"Pas inscrit{ne}."
|
||||
infos["descr_inscription"] = f"Pas inscrit{ne}"
|
||||
else:
|
||||
infos["date_inscription"] = date_inscr
|
||||
infos["descr_inscription"] = f"Inscrit{ne} le {date_inscr}."
|
||||
infos["descr_inscription"] = f"Inscrit{ne} le {date_inscr}"
|
||||
else:
|
||||
infos["date_inscription"] = ""
|
||||
infos["descr_inscription"] = ""
|
||||
|
||||
infos["situation"] = infos["descr_inscription"]
|
||||
infos["descr_defaillance"] = ""
|
||||
|
||||
# Décision: valeurs par defaut vides:
|
||||
infos["decision_jury"] = infos["descr_decision_jury"] = ""
|
||||
infos["decision_sem"] = ""
|
||||
@ -777,13 +778,14 @@ def etud_descr_situation_semestre(
|
||||
infos["descr_demission"] = f"Démission le {date_dem}."
|
||||
infos["date_demission"] = date_dem
|
||||
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
|
||||
if date_def:
|
||||
infos["descr_defaillance"] = f"Défaillant{ne}"
|
||||
infos["date_defaillance"] = date_def
|
||||
infos["descr_decision_jury"] = f"Défaillant{ne}"
|
||||
infos["situation"] += " " + infos["descr_defaillance"]
|
||||
|
||||
dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid])
|
||||
if dpv:
|
||||
@ -794,28 +796,27 @@ def etud_descr_situation_semestre(
|
||||
|
||||
# Décisions de jury:
|
||||
pv = dpv["decisions"][0]
|
||||
dec = ""
|
||||
descr_dec = ""
|
||||
if pv["decision_sem_descr"]:
|
||||
infos["decision_jury"] = pv["decision_sem_descr"]
|
||||
infos["descr_decision_jury"] = (
|
||||
"Décision jury: " + pv["decision_sem_descr"] + ". "
|
||||
)
|
||||
dec = infos["descr_decision_jury"]
|
||||
infos["descr_decision_jury"] = "Décision jury: " + pv["decision_sem_descr"]
|
||||
descr_dec = infos["descr_decision_jury"]
|
||||
else:
|
||||
infos["descr_decision_jury"] = ""
|
||||
infos["decision_jury"] = ""
|
||||
|
||||
if pv["decisions_ue_descr"] and show_uevalid:
|
||||
infos["decisions_ue"] = pv["decisions_ue_descr"]
|
||||
infos["descr_decisions_ue"] = " UE acquises: " + pv["decisions_ue_descr"] + ". "
|
||||
dec += infos["descr_decisions_ue"]
|
||||
infos["descr_decisions_ue"] = " UE acquises: " + pv["decisions_ue_descr"]
|
||||
else:
|
||||
infos["decisions_ue"] = ""
|
||||
infos["descr_decisions_ue"] = ""
|
||||
|
||||
infos["mention"] = pv["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
|
||||
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_niveaux"] = pv.get("descr_decisions_niveaux", "")
|
||||
|
||||
infos["situation"] += " " + dec
|
||||
descr_autorisations = ""
|
||||
if not pv["validation_parcours"]: # parcours non terminé
|
||||
if pv["autorisations_descr"]:
|
||||
infos[
|
||||
"situation"
|
||||
] += f" Autorisé à s'inscrire en {pv['autorisations_descr']}."
|
||||
descr_autorisations = (
|
||||
f"Autorisé à s'inscrire en {pv['autorisations_descr']}."
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
def formsemestre_bulletinetud(
|
||||
etud: Identite = None,
|
||||
|
@ -228,7 +228,7 @@ class TableRecapWithEvalsCache(ScoDocCache):
|
||||
def invalidate_formsemestre( # was inval_cache(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.
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
@ -42,7 +42,7 @@ from app.models import ScolarNews
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
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 sco_codes_parcours
|
||||
@ -100,26 +100,38 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def do_formation_delete(oid):
|
||||
def do_formation_delete(formation_id):
|
||||
"""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]
|
||||
if sco_formations.formation_has_locked_sems(oid):
|
||||
raise ScoLockedFormError()
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# delete all UE in this formation
|
||||
ues = sco_edit_ue.ue_list({"formation_id": oid})
|
||||
for ue in ues:
|
||||
sco_edit_ue.do_ue_delete(ue["ue_id"], force=True)
|
||||
formation: Formation = Formation.query.get(formation_id)
|
||||
if formation is None:
|
||||
return
|
||||
acronyme = formation.acronyme
|
||||
if formation.formsemestres.count():
|
||||
raise ScoNonEmptyFormationObject(
|
||||
type_objet="formation",
|
||||
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
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=oid,
|
||||
text=f"Suppression de la formation {F['acronyme']}",
|
||||
obj=formation_id,
|
||||
text=f"Suppression de la formation {acronyme}",
|
||||
)
|
||||
|
||||
|
||||
|
@ -37,7 +37,14 @@ from app import db
|
||||
from app import log
|
||||
from app.but import apc_edit_ue
|
||||
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.formations import Matiere
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -79,6 +86,7 @@ _ueEditor = ndb.EditableTable(
|
||||
"coefficient",
|
||||
"coef_rcue",
|
||||
"color",
|
||||
"niveau_competence_id",
|
||||
),
|
||||
sortkey="numero",
|
||||
input_formators={
|
||||
@ -138,12 +146,11 @@ def do_ue_create(args):
|
||||
return ue_id
|
||||
|
||||
|
||||
def do_ue_delete(ue_id, delete_validations=False, force=False):
|
||||
"delete UE and attached matieres (but not modules)"
|
||||
from app.scodoc import sco_cursus_dut
|
||||
|
||||
ue = UniteEns.query.get_or_404(ue_id)
|
||||
formation = ue.formation
|
||||
def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
|
||||
"""delete UE and attached matieres (but not modules).
|
||||
Si force, pas de confirmation dialog et pas de redirect
|
||||
"""
|
||||
formation: Formation = ue.formation
|
||||
semestre_idx = ue.semestre_idx
|
||||
if not ue.can_be_deleted():
|
||||
raise ScoNonEmptyFormationObject(
|
||||
@ -157,20 +164,22 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
|
||||
),
|
||||
)
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue.id, delete_validations))
|
||||
# check
|
||||
# if ue_is_locked(ue.id):
|
||||
# raise ScoLockedFormError()
|
||||
log(f"do_ue_delete: ue_id={ue.id}, delete_validations={delete_validations}")
|
||||
|
||||
# Il y a-t-il des etudiants ayant validé cette UE ?
|
||||
# si oui, propose de supprimer les validations
|
||||
validations = sco_cursus_dut.scolar_formsemestre_validation_list(
|
||||
cnx, args={"ue_id": ue.id}
|
||||
)
|
||||
if validations and not delete_validations and not force:
|
||||
validations_ue = ScolarFormSemestreValidation.query.filter_by(ue_id=ue.id).all()
|
||||
validations_rcue = ApcValidationRCUE.query.filter(
|
||||
(ApcValidationRCUE.ue1_id == ue.id) | (ApcValidationRCUE.ue2_id == ue.id)
|
||||
).all()
|
||||
if (
|
||||
(len(validations_ue) > 0 or len(validations_rcue) > 0)
|
||||
and not delete_validations
|
||||
and not force
|
||||
):
|
||||
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>"
|
||||
% (len(validations), ue.acronyme, ue.titre),
|
||||
f"""<p>Des étudiants ont une décision de jury sur l'UE {ue.acronyme} ({ue.titre})</p>
|
||||
<p>Si vous supprimez cette UE, ces décisions vont être supprimées !</p>""",
|
||||
dest_url="",
|
||||
target_variable="delete_validations",
|
||||
cancel_url=url_for(
|
||||
@ -183,31 +192,34 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
|
||||
)
|
||||
if delete_validations:
|
||||
log(f"deleting all validations of UE {ue.id}")
|
||||
ndb.SimpleQuery(
|
||||
"DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
|
||||
{"ue_id": ue.id},
|
||||
)
|
||||
for v in validations_ue:
|
||||
db.session.delete(v)
|
||||
for v in validations_rcue:
|
||||
db.session.delete(v)
|
||||
|
||||
# delete old formulas
|
||||
ndb.SimpleQuery(
|
||||
"DELETE FROM notes_formsemestre_ue_computation_expr WHERE ue_id=%(ue_id)s",
|
||||
{"ue_id": ue.id},
|
||||
)
|
||||
# delete all matiere in this UE
|
||||
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
|
||||
for mat in mats:
|
||||
sco_edit_matiere.do_matiere_delete(mat["matiere_id"])
|
||||
# delete uecoef and events
|
||||
ndb.SimpleQuery(
|
||||
"DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s",
|
||||
{"ue_id": ue.id},
|
||||
)
|
||||
ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue.id})
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_ueEditor.delete(cnx, ue.id)
|
||||
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement
|
||||
# utilisé: acceptable de tout invalider):
|
||||
formulas = FormSemestreUEComputationExpr.query.filter_by(ue_id=ue.id).all()
|
||||
for formula in formulas:
|
||||
db.session.delete(formula)
|
||||
|
||||
# delete all matieres in this UE
|
||||
for mat in Matiere.query.filter_by(ue_id=ue.id):
|
||||
db.session.delete(mat)
|
||||
|
||||
# delete uecoefs
|
||||
for uecoef in FormSemestreUECoef.query.filter_by(ue_id=ue.id):
|
||||
db.session.delete(uecoef)
|
||||
# delete events
|
||||
for event in ScolarEvent.query.filter_by(ue_id=ue.id):
|
||||
db.session.delete(event)
|
||||
db.session.flush()
|
||||
|
||||
db.session.delete(ue)
|
||||
db.session.commit()
|
||||
|
||||
# cas compliqué, mais rarement utilisé: acceptable de tout invalider
|
||||
formation.invalidate_module_coefs()
|
||||
# -> invalide aussi .invalidate_formsemestre()
|
||||
# -> invalide aussi les formsemestres
|
||||
# news
|
||||
ScolarNews.add(
|
||||
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
|
||||
|
@ -658,8 +658,10 @@ def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
|
||||
info = sco_etape_apogee.apo_csv_get_archive(
|
||||
etape_apo, semset["annee_scolaire"], semset["sem_id"]
|
||||
)
|
||||
if info:
|
||||
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%20inexistante")
|
||||
|
||||
|
||||
def view_apo_csv(etape_apo="", semset_id="", format="html"):
|
||||
|
@ -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"""
|
||||
|
||||
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}).
|
||||
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.
|
||||
|
@ -31,7 +31,7 @@ from operator import itemgetter
|
||||
import xml.dom.minidom
|
||||
|
||||
import flask
|
||||
from flask import g, url_for, request
|
||||
from flask import flash, g, url_for, request
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -65,6 +65,7 @@ _formationEditor = ndb.EditableTable(
|
||||
"formation_code",
|
||||
"type_parcours",
|
||||
"code_specialite",
|
||||
"referentiel_competence_id",
|
||||
),
|
||||
filter_dept=True,
|
||||
sortkey="acronyme",
|
||||
@ -104,7 +105,7 @@ def formation_export(
|
||||
"""Get a formation, with UE, matieres, modules
|
||||
in desired format
|
||||
"""
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
formation: Formation = Formation.query.get_or_404(formation_id)
|
||||
F = formation.to_dict()
|
||||
selector = {"formation_id": formation_id}
|
||||
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):
|
||||
"duplicate formation, with new version number"
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
resp = formation_export(formation_id, export_ids=True, format="xml")
|
||||
xml_data = resp.get_data(as_text=True)
|
||||
new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data)
|
||||
# news
|
||||
F = formation_list(args={"formation_id": new_id})[0]
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=new_id,
|
||||
text="Nouvelle version de la formation %(acronyme)s" % F,
|
||||
text=f"Nouvelle version de la formation {formation.acronyme}",
|
||||
)
|
||||
if redirect:
|
||||
flash("Nouvelle version !")
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.ue_table",
|
||||
|
@ -746,7 +746,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
||||
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>'
|
||||
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>'
|
||||
|
||||
|
@ -679,18 +679,12 @@ def formsemestre_recap_parcours_table(
|
||||
class_ue = "ue"
|
||||
if ue_status and ue_status["is_external"]: # validation 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"]:
|
||||
class_ue += " ue_capitalized"
|
||||
explanation_ue.append(
|
||||
"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(
|
||||
'<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"])
|
||||
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(
|
||||
'<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};"> </td>
|
||||
<td></td>"""
|
||||
)
|
||||
# Total ECTS (affiché sous la moyenne générale)
|
||||
H.append(
|
||||
'<td class="rcp_type_sem" style="background-color:%s;"> </td><td></td>'
|
||||
% (bgcolor)
|
||||
f"""<td class="sem_ects_tit"><a title="crédit acquis">ECTS:</a></td>
|
||||
<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
|
||||
for ue in ues:
|
||||
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(
|
||||
'<td class="ue">%g</td>'
|
||||
% (ue_status["ects_pot"] if ue_status else "")
|
||||
f"""<td class="ue" title="{ects:2.2g}/{ects_pot:2.2g} ECTS">{ects:2.2g}</td>"""
|
||||
)
|
||||
else:
|
||||
H.append(f"""<td class="ue"></td>""")
|
||||
H.append("<td></td></tr>")
|
||||
|
||||
H.append("</table>")
|
||||
|
@ -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):
|
||||
result = filename_parser.match(entry.name)
|
||||
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()
|
||||
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
|
||||
if self.density is not None:
|
||||
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]
|
||||
x_mm = round(x_size * unit2mm / x_density, 2)
|
||||
y_mm = round(y_size * unit2mm / y_density, 2)
|
||||
@ -244,7 +246,6 @@ class Logo:
|
||||
self.mm = None
|
||||
else:
|
||||
self.mm = None
|
||||
|
||||
self.size = (x_size, y_size)
|
||||
self.aspect_ratio = round(float(x_size) / y_size, 2)
|
||||
|
||||
|
@ -206,6 +206,7 @@ def dict_pvjury(
|
||||
{
|
||||
'date' : date de la decision la plus recente,
|
||||
'formsemestre' : sem,
|
||||
'is_apc' : bool,
|
||||
'formation' : { 'acronyme' :, 'titre': ... }
|
||||
'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,},
|
||||
'etat' : I ou D ou DEF
|
||||
@ -359,6 +360,7 @@ def dict_pvjury(
|
||||
return {
|
||||
"date": ndb.DateISOtoDMY(max_date),
|
||||
"formsemestre": sem,
|
||||
"is_apc": nt.is_apc,
|
||||
"has_prev": has_prev,
|
||||
"semestre_non_terminal": semestre_non_terminal,
|
||||
"formation": sco_formations.formation_list(
|
||||
@ -418,7 +420,10 @@ def pvjury_table(
|
||||
titles["prev_decision"] = "Décision S%s" % id_prev
|
||||
columns_ids += ["prev_decision"]
|
||||
|
||||
if not dpv["is_apc"]:
|
||||
# Décision de jury sur le semestre, sauf en BUT
|
||||
columns_ids += ["decision"]
|
||||
|
||||
if sco_preferences.get_preference("bul_show_mention", formsemestre_id):
|
||||
columns_ids += ["mention"]
|
||||
columns_ids += ["ue_cap"]
|
||||
@ -444,7 +449,7 @@ def pvjury_table(
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
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"],
|
||||
"decision": _descr_decision_sem_abbrev(e["etat"], e["decision_sem"]),
|
||||
"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
|
||||
# XXX TODO
|
||||
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
|
||||
|
||||
return jury_but_recap.formsemestre_saisie_jury_but(
|
||||
|
@ -2,57 +2,75 @@
|
||||
/*******************/
|
||||
/* Styles généraux */
|
||||
/*******************/
|
||||
.wait{
|
||||
.wait {
|
||||
width: 60px;
|
||||
height: 6px;
|
||||
margin: auto;
|
||||
background: #424242; /* la réponse à tout */
|
||||
background: #424242;
|
||||
/* la réponse à tout */
|
||||
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;
|
||||
--couleurFondTitresRes: #f8c844;
|
||||
--couleurFondTitresSAE: #c6ffab;
|
||||
--couleurSecondaire: #fec;
|
||||
--couleurIntense: rgb(4, 16, 159);;
|
||||
--couleurIntense: rgb(4, 16, 159);
|
||||
;
|
||||
--couleurSurlignage: rgba(255, 253, 110, 0.49);
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
display: none;
|
||||
}
|
||||
.releve a, .releve a:visited {
|
||||
|
||||
.releve a,
|
||||
.releve a:visited {
|
||||
color: navy;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.releve a:hover {
|
||||
color: red;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ready .wait{display: none;}
|
||||
.ready main{display: block;}
|
||||
h2{
|
||||
.ready .wait {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ready main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: black;
|
||||
}
|
||||
section{
|
||||
|
||||
section {
|
||||
background: #FFF;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #AAA;
|
||||
padding: 16px 32px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
section>div:nth-child(1){
|
||||
|
||||
section>div:nth-child(1) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.CTA_Liste{
|
||||
|
||||
.CTA_Liste {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
@ -60,40 +78,46 @@ section>div:nth-child(1){
|
||||
color: #FFF;
|
||||
padding: 4px 8px;
|
||||
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;
|
||||
}
|
||||
.CTA_Liste>svg{
|
||||
|
||||
.CTA_Liste>svg {
|
||||
transition: 0.2s;
|
||||
}
|
||||
.CTA_Liste:hover{
|
||||
|
||||
.CTA_Liste:hover {
|
||||
outline: 2px solid #424242;
|
||||
}
|
||||
.listeOff svg{
|
||||
|
||||
.listeOff svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.listeOff .syntheseModule,
|
||||
.listeOff .eval{
|
||||
.listeOff .eval {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.moduleOnOff>.syntheseModule,
|
||||
.moduleOnOff>.eval{
|
||||
.moduleOnOff>.eval {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.listeOff .moduleOnOff>.syntheseModule,
|
||||
.listeOff .moduleOnOff>.eval{
|
||||
.listeOff .moduleOnOff>.eval {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.listeOff .ue::before,
|
||||
.listeOff .module::before,
|
||||
.moduleOnOff .ue::before,
|
||||
.moduleOnOff .module::before{
|
||||
.moduleOnOff .module::before {
|
||||
transform: rotate(0);
|
||||
}
|
||||
|
||||
.listeOff .moduleOnOff .ue::before,
|
||||
.listeOff .moduleOnOff .module::before{
|
||||
.listeOff .moduleOnOff .module::before {
|
||||
transform: rotate(180deg) !important;
|
||||
}
|
||||
|
||||
@ -107,24 +131,25 @@ section>div:nth-child(1){
|
||||
.hide_coef .eval>em,
|
||||
.hide_date_inscr .dateInscription,
|
||||
.hide_ects .ects,
|
||||
.hide_rangs .rang{
|
||||
.hide_rangs .rang {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/*.module>.absences,*/
|
||||
.module .moyenne,
|
||||
.module .info{
|
||||
.module .info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/************/
|
||||
/* Etudiant */
|
||||
/************/
|
||||
.info_etudiant{
|
||||
.info_etudiant {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.etudiant{
|
||||
|
||||
.etudiant {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
@ -132,7 +157,8 @@ section>div:nth-child(1){
|
||||
background: var(--couleurPrincipale);
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
.civilite{
|
||||
|
||||
.civilite {
|
||||
font-weight: bold;
|
||||
font-size: 130%;
|
||||
}
|
||||
@ -140,19 +166,21 @@ section>div:nth-child(1){
|
||||
/************/
|
||||
/* Semestre */
|
||||
/************/
|
||||
.flex{
|
||||
.flex {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.infoSemestre{
|
||||
|
||||
.infoSemestre {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
.infoSemestre>div{
|
||||
|
||||
.infoSemestre>div {
|
||||
border: 1px solid var(--couleurIntense);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
@ -162,48 +190,61 @@ section>div:nth-child(1){
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.infoSemestre>div>div:nth-child(even){
|
||||
.infoSemestre>div>div:nth-child(even) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.photo {
|
||||
border: none;
|
||||
margin-left: auto;
|
||||
}
|
||||
.rang, .competence{
|
||||
|
||||
.rang,
|
||||
.competence {
|
||||
font-weight: bold;
|
||||
}
|
||||
.ue .rang{
|
||||
|
||||
.ue .rang {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.absencesRecap {
|
||||
align-items: baseline;
|
||||
}
|
||||
.absencesRecap > div:nth-child(2n) {
|
||||
|
||||
.absencesRecap>div:nth-child(2n) {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.abs {
|
||||
font-weight: bold;
|
||||
}
|
||||
.decision{
|
||||
|
||||
.decision {
|
||||
margin: 5px 0;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
#ects_tot, .decision, .decision_annee {
|
||||
|
||||
#ects_tot,
|
||||
.decision,
|
||||
.decision_annee {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
font-size: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.enteteSemestre{
|
||||
|
||||
.enteteSemestre {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
font-size: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/***************/
|
||||
/* Zone custom */
|
||||
/***************/
|
||||
.custom:empty{
|
||||
.custom:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -211,20 +252,23 @@ section>div:nth-child(1){
|
||||
/* Synthèse */
|
||||
/***************/
|
||||
.synthese .ue,
|
||||
.synthese h3{
|
||||
.synthese h3 {
|
||||
background: var(--couleurFondTitresUE);
|
||||
}
|
||||
.synthese .ue>div{
|
||||
|
||||
.synthese .ue>div {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.synthese em,
|
||||
.eval em{
|
||||
.eval em {
|
||||
opacity: 0.6;
|
||||
min-width: 80px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ueBonus,
|
||||
.ueBonus h3{
|
||||
.ueBonus h3 {
|
||||
background: var(--couleurFondTitresSAE) !important;
|
||||
color: #000 !important;
|
||||
}
|
||||
@ -233,10 +277,12 @@ section>div:nth-child(1){
|
||||
/* Evaluations */
|
||||
/***************/
|
||||
.evaluations>div,
|
||||
.sae>div{
|
||||
.sae>div {
|
||||
scroll-margin-top: 60px;
|
||||
}
|
||||
.module, .ue {
|
||||
|
||||
.module,
|
||||
.ue {
|
||||
color: #000;
|
||||
padding: 4px 32px;
|
||||
border-radius: 4px;
|
||||
@ -248,17 +294,22 @@ section>div:nth-child(1){
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ue {
|
||||
background: var(--couleurFondTitresRes);
|
||||
}
|
||||
|
||||
.module {
|
||||
background: var(--couleurFondTitresRes);
|
||||
}
|
||||
|
||||
.module h3 {
|
||||
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;
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
@ -267,14 +318,18 @@ section>div:nth-child(1){
|
||||
transform: rotate(180deg);
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
|
||||
/* Placer le chevron à gauche au lieu du milieu */
|
||||
.module::before, .ue::before {
|
||||
.module::before,
|
||||
.ue::before {
|
||||
left: 2px;
|
||||
bottom: calc(50% - 13px);
|
||||
}
|
||||
}
|
||||
h3{
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 auto 0 0;
|
||||
@ -284,21 +339,27 @@ h3{
|
||||
font-size: 16px;
|
||||
background: var(--couleurSecondaire);
|
||||
}
|
||||
.sae .module, .sae h3{
|
||||
|
||||
.sae .module,
|
||||
.sae h3 {
|
||||
background: var(--couleurFondTitresSAE);
|
||||
}
|
||||
|
||||
.moyenne{
|
||||
.moyenne {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
.info{
|
||||
|
||||
.info {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.syntheseModule{
|
||||
|
||||
.syntheseModule {
|
||||
cursor: pointer;
|
||||
}
|
||||
.eval, .syntheseModule{
|
||||
|
||||
.eval,
|
||||
.syntheseModule {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -306,17 +367,21 @@ h3{
|
||||
padding: 0px 4px;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
.eval>div, .syntheseModule>div{
|
||||
|
||||
.eval>div,
|
||||
.syntheseModule>div {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.eval:hover, .syntheseModule:hover{
|
||||
.eval:hover,
|
||||
.syntheseModule:hover {
|
||||
background: var(--couleurSurlignage);
|
||||
/* color: #FFF; */
|
||||
}
|
||||
.complement{
|
||||
pointer-events:none;
|
||||
|
||||
.complement {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
@ -331,30 +396,37 @@ h3{
|
||||
gap: 0 !important;
|
||||
column-gap: 4px !important;
|
||||
}
|
||||
.eval:hover .complement{
|
||||
|
||||
.eval:hover .complement {
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
.complement>div:nth-child(even){
|
||||
|
||||
.complement>div:nth-child(even) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.complement>div:nth-child(1),
|
||||
.complement>div:nth-child(2){
|
||||
.complement>div:nth-child(2) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.complement>div:nth-child(1),
|
||||
.complement>div:nth-child(7){
|
||||
.complement>div:nth-child(7) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
section{
|
||||
section {
|
||||
padding: 16px;
|
||||
}
|
||||
.syntheseModule, .eval {
|
||||
|
||||
.syntheseModule,
|
||||
.eval {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*.absences{
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
|
@ -46,9 +46,29 @@ function set_ue_niveau_competence(elem) {
|
||||
niveau_id: niveau_id,
|
||||
},
|
||||
function (result) {
|
||||
alert("niveau de compétence enregistré"); // XXX #frontend à améliorer
|
||||
// obj.classList.remove("sco_wait");
|
||||
// obj.classList.add("sco_modified");
|
||||
// alert("niveau de compétence enregistré"); // XXX #frontend à améliorer
|
||||
sco_message("niveau de compétence enregistré");
|
||||
|
||||
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 != "");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
@ -85,7 +85,7 @@ function sco_message(msg, color) {
|
||||
}
|
||||
);
|
||||
},
|
||||
2000 // <-- duree affichage en milliseconds
|
||||
3000 // <-- duree affichage en milliseconds
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.3.20"
|
||||
SCOVERSION = "9.3.22"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -327,7 +327,7 @@ def test_formations(test_client):
|
||||
|
||||
# --- 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)
|
||||
assert isinstance(lif3, str)
|
||||
load_lif3 = json.loads(lif3)
|
||||
|
Loading…
Reference in New Issue
Block a user