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)
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}"

View File

@ -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(

View File

@ -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)

View File

@ -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
"""

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_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",

View File

@ -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.

View File

@ -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.
"""

View File

@ -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))

View File

@ -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,

View File

@ -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]:

View File

@ -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}

View File

@ -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
)

View File

@ -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 {

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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}",
)

View File

@ -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

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(
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"):

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"""
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.

View File

@ -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",

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"]):
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>'

View File

@ -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};">&nbsp;</td>
<td></td>"""
)
# Total ECTS (affiché sous la moyenne générale)
H.append(
'<td class="rcp_type_sem" style="background-color:%s;">&nbsp;</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>")

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):
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)

View File

@ -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(

View File

@ -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;

View File

@ -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 != "");
}
}
);
}

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 -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.3.20"
SCOVERSION = "9.3.22"
SCONAME = "ScoDoc"

View File

@ -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)