This commit is contained in:
leonard_montalbano 2022-07-08 14:28:34 +02:00
commit e822ab4da0
55 changed files with 1716 additions and 478 deletions

0
LICENSE Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

View File

@ -212,7 +212,9 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
formsemestres = query.order_by(FormSemestre.date_debut) formsemestres = query.order_by(FormSemestre.date_debut)
return jsonify([formsemestre.to_dict() for formsemestre in formsemestres]) return jsonify(
[formsemestre.to_dict(convert_parcours=True) for formsemestre in formsemestres]
)
@bp.route( @bp.route(
@ -471,7 +473,7 @@ def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner
return response return response
return sco_bulletins.get_formsemestre_bulletin_etud_json( return sco_bulletins.get_formsemestre_bulletin_etud_json(
formsemestre, etud, version formsemestre, etud, version=version
) )

View File

@ -62,7 +62,7 @@ def formsemestre(formsemestre_id: int):
formsemestre: FormSemestre = models.FormSemestre.query.filter_by( formsemestre: FormSemestre = models.FormSemestre.query.filter_by(
id=formsemestre_id id=formsemestre_id
).first_or_404() ).first_or_404()
data = formsemestre.to_dict() data = formsemestre.to_dict(convert_parcours=True)
# Pour le moment on a besoin de fixer le departement # Pour le moment on a besoin de fixer le departement
# pour accéder aux préferences # pour accéder aux préferences
dept = Departement.query.get(formsemestre.dept_id) dept = Departement.query.get(formsemestre.dept_id)
@ -92,13 +92,9 @@ def formsemestre_apo(etape_apo: str):
FormSemestreEtape.formsemestre_id == FormSemestre.id, FormSemestreEtape.formsemestre_id == FormSemestre.id,
) )
res = [formsemestre.to_dict() for formsemestre in formsemestres] return jsonify(
if len(res) == 0: [formsemestre.to_dict(convert_parcours=True) for formsemestre in formsemestres]
return error_response( )
404, message="Aucun formsemestre trouvé avec cette étape apogée"
)
else:
return jsonify(res)
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"]) @bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"])

View File

@ -14,6 +14,7 @@ from flask import url_for, g
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite from app.models import FormSemestre, Identite
from app.models import but_validations
from app.models.groups import GroupDescr from app.models.groups import GroupDescr
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins, sco_utils as scu
@ -323,9 +324,13 @@ class BulletinBUT:
ects_tot = sum([ue.ects or 0 for ue in res.ues]) if res.ues else 0.0 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_acquis = sum([d.get("ects", 0) for d in decisions_ues.values()])
semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot} semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
semestre_infos.update( if sco_preferences.get_preference("bul_show_decision", formsemestre.id):
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id) semestre_infos.update(
) sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
)
semestre_infos.update(
but_validations.dict_decision_jury(etud, formsemestre)
)
if etat_inscription == scu.INSCRIT: if etat_inscription == scu.INSCRIT:
# moyenne des moyennes générales du semestre # moyenne des moyennes générales du semestre
semestre_infos["notes"] = { semestre_infos["notes"] = {

View File

@ -68,7 +68,7 @@ from flask import g, url_for
from app import db from app import db
from app import log from app import log
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem from app.comp import inscr_mod, res_sem
from app.models import formsemestre from app.models import formsemestre
from app.models.but_refcomp import ( from app.models.but_refcomp import (
@ -189,6 +189,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.RAT, sco_codes.RAT,
sco_codes.ABAN, sco_codes.ABAN,
sco_codes.ABL, sco_codes.ABL,
sco_codes.ATJ,
sco_codes.DEF, sco_codes.DEF,
sco_codes.DEM, sco_codes.DEM,
sco_codes.EXCLU, sco_codes.EXCLU,
@ -200,6 +201,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
formsemestre: FormSemestre, formsemestre: FormSemestre,
): ):
super().__init__(etud=etud) super().__init__(etud=etud)
self.formsemestre_id = formsemestre.id
formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre) formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre)
assert ( assert (
(formsemestre_pair is None) (formsemestre_pair is None)
@ -217,15 +219,16 @@ class DecisionsProposeesAnnee(DecisionsProposees):
"le 1er semestre de l'année scolaire considérée (S1, S3, S5)" "le 1er semestre de l'année scolaire considérée (S1, S3, S5)"
self.formsemestre_pair = formsemestre_pair self.formsemestre_pair = formsemestre_pair
"le second formsemestre de la même année scolaire (S2, S4, S6)" "le second formsemestre de la même année scolaire (S2, S4, S6)"
self.annee_but = ( formsemestre_last = formsemestre_pair or formsemestre_impair
formsemestre_impair.semestre_id // 2 + 1 "le formsemestre le plus avancé dans cette année"
if formsemestre_impair
else formsemestre_pair.semestre_id // 2 self.annee_but = (formsemestre_last.semestre_id + 1) // 2
)
"le rang de l'année dans le BUT: 1, 2, 3" "le rang de l'année dans le BUT: 1, 2, 3"
assert self.annee_but in (1, 2, 3) assert self.annee_but in (1, 2, 3)
self.rcues_annee = [] self.rcues_annee = []
"RCUEs de l'année" "RCUEs de l'année"
self.inscription_etat = etud.inscription_etat(formsemestre_last.id)
if self.formsemestre_impair is not None: if self.formsemestre_impair is not None:
self.validation = ApcValidationAnnee.query.filter_by( self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id, etudid=self.etud.id,
@ -253,13 +256,17 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all
self.decisions_ues = { self.decisions_ues = {
ue.id: DecisionsProposeesUE(etud, formsemestre_impair, ue) ue.id: DecisionsProposeesUE(
etud, formsemestre_impair, ue, self.inscription_etat
)
for ue in self.ues_impair for ue in self.ues_impair
} }
"{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année" "{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année"
self.decisions_ues.update( self.decisions_ues.update(
{ {
ue.id: DecisionsProposeesUE(etud, formsemestre_pair, ue) ue.id: DecisionsProposeesUE(
etud, formsemestre_pair, ue, self.inscription_etat
)
for ue in self.ues_pair for ue in self.ues_pair
} }
) )
@ -289,8 +296,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
[rcue for rcue in rcues_avec_niveau if not rcue.est_suffisant()] [rcue for rcue in rcues_avec_niveau if not rcue.est_suffisant()]
) )
"le nb de comp. sous la barre de 8/20" "le nb de comp. sous la barre de 8/20"
# année ADM si toutes RCUE validées (sinon PASD) # année ADM si toutes RCUE validées (sinon PASD) et non DEM ou DEF
self.admis = self.nb_validables == self.nb_competences self.admis = (self.nb_validables == self.nb_competences) and (
self.inscription_etat == scu.INSCRIT
)
"vrai si l'année est réussie, tous niveaux validables" "vrai si l'année est réussie, tous niveaux validables"
self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2) self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2)
# Peut passer si plus de la moitié validables et tous > 8 # Peut passer si plus de la moitié validables et tous > 8
@ -308,6 +317,19 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.admis: if self.admis:
self.codes = [sco_codes.ADM] + self.codes self.codes = [sco_codes.ADM] + self.codes
self.explanation = expl_rcues self.explanation = expl_rcues
elif self.inscription_etat != scu.INSCRIT:
self.codes = [
sco_codes.DEM
if self.inscription_etat == scu.DEMISSION
else sco_codes.DEF,
# propose aussi d'autres codes, au cas où...
sco_codes.DEM
if self.inscription_etat != scu.DEMISSION
else sco_codes.DEF,
sco_codes.ABAN,
sco_codes.ABL,
sco_codes.EXCLU,
]
elif self.passage_de_droit: elif self.passage_de_droit:
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
self.explanation = expl_rcues self.explanation = expl_rcues
@ -385,7 +407,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def comp_formsemestres( def comp_formsemestres(
self, formsemestre: FormSemestre self, formsemestre: FormSemestre
) -> tuple[FormSemestre, FormSemestre]: ) -> tuple[FormSemestre, FormSemestre]:
"les deux formsemestres de l'année scolaire à laquelle appartient formsemestre" """les deux formsemestres de l'année scolaire à laquelle appartient formsemestre."""
if not formsemestre.formation.is_apc(): # garde fou
return None, None
if formsemestre.semestre_id % 2 == 0: if formsemestre.semestre_id % 2 == 0:
other_semestre_id = formsemestre.semestre_id - 1 other_semestre_id = formsemestre.semestre_id - 1
else: else:
@ -419,7 +443,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
(self.formsemestre_impair, self.res_impair), (self.formsemestre_impair, self.res_impair),
(self.formsemestre_pair, self.res_pair), (self.formsemestre_pair, self.res_pair),
): ):
if formsemestre is None: if (formsemestre is None) or (not formsemestre.formation.is_apc()):
ues = [] ues = []
else: else:
formation: Formation = formsemestre.formation formation: Formation = formsemestre.formation
@ -478,6 +502,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
ue_impair, ue_impair,
self.formsemestre_pair, self.formsemestre_pair,
ue_pair, ue_pair,
self.inscription_etat,
) )
ues_impair_sans_rcue.discard(ue_impair.id) ues_impair_sans_rcue.discard(ue_impair.id)
break break
@ -505,7 +530,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
rcue = rc rcue = rc
break break
if rcue is not None: if rcue is not None:
dec_rcue = DecisionsProposeesRCUE(self, rcue) dec_rcue = DecisionsProposeesRCUE(self, rcue, self.inscription_etat)
rc_niveaux.append((dec_rcue, niveau.id)) rc_niveaux.append((dec_rcue, niveau.id))
# prévient les UE concernées :-) # prévient les UE concernées :-)
self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue) self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue)
@ -663,6 +688,46 @@ class DecisionsProposeesAnnee(DecisionsProposees):
db.session.delete(validation) db.session.delete(validation)
db.session.flush() db.session.flush()
def get_autorisations_passage(self) -> list[int]:
"""Les liste des indices de semestres auxquels on est autorisé à
s'inscrire depuis cette année"""
formsemestre = self.formsemestre_pair or self.formsemestre_impair
if not formsemestre:
return []
return [
a.semestre_id
for a in ScolarAutorisationInscription.query.filter_by(
etudid=self.etud.id,
origin_formsemestre_id=formsemestre.id,
)
]
def descr_niveaux_validation(self, line_sep: str = "\n") -> str:
"""Description textuelle des niveaux validés (enregistrés)
pour PV jurys
"""
validations = [
dec_rcue.descr_validation()
for dec_rcue in self.decisions_rcue_by_niveau.values()
]
return line_sep.join(v for v in validations if v)
def descr_ues_validation(self, line_sep: str = "\n") -> str:
"""Description textuelle des UE validées (enregistrés)
pour PV jurys
"""
validations = []
for res in (self.res_impair, self.res_pair):
if res:
dec_ues = [
self.decisions_ues[ue.id]
for ue in res.ues
if ue.type == UE_STANDARD and ue.id in self.decisions_ues
]
valids = [dec_ue.descr_validation() for dec_ue in dec_ues]
validations.append(", ".join(v for v in valids if v))
return line_sep.join(validations)
class DecisionsProposeesRCUE(DecisionsProposees): class DecisionsProposeesRCUE(DecisionsProposees):
"""Liste des codes de décisions que l'on peut proposer pour """Liste des codes de décisions que l'on peut proposer pour
@ -673,20 +738,33 @@ class DecisionsProposeesRCUE(DecisionsProposees):
codes_communs = [ codes_communs = [
sco_codes.ADJ, sco_codes.ADJ,
sco_codes.ATJ,
sco_codes.RAT, sco_codes.RAT,
sco_codes.DEF, sco_codes.DEF,
sco_codes.ABAN, sco_codes.ABAN,
] ]
def __init__( def __init__(
self, dec_prop_annee: DecisionsProposeesAnnee, rcue: RegroupementCoherentUE self,
dec_prop_annee: DecisionsProposeesAnnee,
rcue: RegroupementCoherentUE,
inscription_etat: str = scu.INSCRIT,
): ):
super().__init__(etud=dec_prop_annee.etud) super().__init__(etud=dec_prop_annee.etud)
self.rcue = rcue self.rcue = rcue
if rcue is None: # RCUE non dispo, eg un seul semestre if rcue is None: # RCUE non dispo, eg un seul semestre
self.codes = [] self.codes = []
return return
self.inscription_etat = inscription_etat
"inscription: I, DEM, DEF"
self.parcour = dec_prop_annee.parcour self.parcour = dec_prop_annee.parcour
if inscription_etat != scu.INSCRIT:
self.validation = None # cache toute validation
self.explanation = "non incrit (dem. ou déf.)"
self.codes = [
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
]
return
self.validation = rcue.query_validations().first() self.validation = rcue.query_validations().first()
if self.validation is not None: if self.validation is not None:
self.code_valide = self.validation.code self.code_valide = self.validation.code
@ -737,6 +815,21 @@ class DecisionsProposeesRCUE(DecisionsProposees):
db.session.delete(validation) db.session.delete(validation)
db.session.flush() db.session.flush()
def descr_validation(self) -> str:
"""Description validation niveau enregistrée, pour PV jury.
Si le niveau est validé, done son acronyme, sinon chaine vide.
"""
if self.code_valide in sco_codes.CODES_RCUE_VALIDES:
if (
self.rcue and self.rcue.ue_1 and self.rcue.ue_1.niveau_competence
): # prudence !
niveau_titre = self.rcue.ue_1.niveau_competence.competence.titre or ""
ordre = self.rcue.ue_1.niveau_competence.ordre
else:
return "?" # oups ?
return f"{niveau_titre} niv. {ordre}"
return ""
class DecisionsProposeesUE(DecisionsProposees): class DecisionsProposeesUE(DecisionsProposees):
"""Décisions de jury sur une UE du BUT """Décisions de jury sur une UE du BUT
@ -758,6 +851,7 @@ class DecisionsProposeesUE(DecisionsProposees):
sco_codes.RAT, sco_codes.RAT,
sco_codes.DEF, sco_codes.DEF,
sco_codes.ABAN, sco_codes.ABAN,
sco_codes.ATJ,
sco_codes.DEM, sco_codes.DEM,
sco_codes.UEBSL, sco_codes.UEBSL,
] ]
@ -767,12 +861,27 @@ class DecisionsProposeesUE(DecisionsProposees):
etud: Identite, etud: Identite,
formsemestre: FormSemestre, formsemestre: FormSemestre,
ue: UniteEns, ue: UniteEns,
inscription_etat: str = scu.INSCRIT,
): ):
super().__init__(etud=etud) super().__init__(etud=etud)
self.formsemestre = formsemestre self.formsemestre = formsemestre
self.ue: UniteEns = ue self.ue: UniteEns = ue
self.rcue: RegroupementCoherentUE = None self.rcue: RegroupementCoherentUE = None
"Le rcu auquel est rattaché cette UE, ou None" "Le rcu auquel est rattaché cette UE, ou None"
self.inscription_etat = inscription_etat
"inscription: I, DEM, DEF"
if ue.type == sco_codes.UE_SPORT:
self.explanation = "UE bonus, pas de décision de jury"
self.codes = [] # aucun code proposé
return
if inscription_etat != scu.INSCRIT:
self.validation = None # cache toute validation
self.explanation = "non incrit (dem. ou déf.)"
self.codes = [
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
]
self.moy_ue = "-"
return
# Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non) # 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) # mais ici on a restreint au formsemestre donc une seule (prend la première)
self.validation = ScolarFormSemestreValidation.query.filter_by( self.validation = ScolarFormSemestreValidation.query.filter_by(
@ -780,10 +889,6 @@ class DecisionsProposeesUE(DecisionsProposees):
).first() ).first()
if self.validation is not None: if self.validation is not None:
self.code_valide = self.validation.code self.code_valide = self.validation.code
if ue.type == sco_codes.UE_SPORT:
self.explanation = "UE bonus, pas de décision de jury"
self.codes = [] # aucun code proposé
return
# Moyenne de l'UE ? # Moyenne de l'UE ?
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
@ -802,6 +907,8 @@ class DecisionsProposeesUE(DecisionsProposees):
def compute_codes(self): def compute_codes(self):
"""Calcul des .codes attribuables et de l'explanation associée""" """Calcul des .codes attribuables et de l'explanation associée"""
if self.inscription_etat != scu.INSCRIT:
return
if self.moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE): if self.moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE):
self.codes.insert(0, sco_codes.ADM) self.codes.insert(0, sco_codes.ADM)
self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",) self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",)
@ -853,6 +960,14 @@ class DecisionsProposeesUE(DecisionsProposees):
db.session.delete(validation) db.session.delete(validation)
db.session.flush() db.session.flush()
def descr_validation(self) -> str:
"""Description validation niveau enregistrée, pour PV jury.
Si l'UE est validée, donne son acronyme, sinon chaine vide.
"""
if self.code_valide in sco_codes.CODES_UE_VALIDES:
return f"{self.ue.acronyme}"
return ""
class BUTCursusEtud: # WIP TODO class BUTCursusEtud: # WIP TODO
"""Validation du cursus d'un étudiant""" """Validation du cursus d'un étudiant"""
@ -932,7 +1047,7 @@ class BUTCursusEtud: # WIP TODO
"""La liste des UE à valider si on valide ce niveau. """La liste des UE à valider si on valide ce niveau.
Ne liste que les UE qui ne sont pas déjà acquises. Ne liste que les UE qui ne sont pas déjà acquises.
Selon la règle donéne par l'arrêté BUT: Selon la règle donnée par l'arrêté BUT:
* La validation des deux UE du niveau dune compétence emporte la validation de * La validation des deux UE du niveau dune compétence emporte la validation de
l'ensemble des UE du niveau inférieur de cette même compétence. l'ensemble des UE du niveau inférieur de cette même compétence.
""" """

137
app/but/jury_but_pv.py Normal file
View File

@ -0,0 +1,137 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Jury BUT: table synthèse résultats semestre / PV
"""
from flask import g, request, url_for
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from app import log
from app.but import jury_but
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_excel
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
def _descr_cursus_but(etud: Identite) -> str:
"description de la liste des semestres BUT suivis"
# prend simplement tous les semestre de type APC, ce qui sera faux si
# l'étudiant change de spécialité au sein du même département
# (ce qui ne peut normalement pas se produire)
indices = sorted(
[
ins.formsemestre.semestre_id
if ins.formsemestre.semestre_id is not None
else -1
for ins in etud.formsemestre_inscriptions
if ins.formsemestre.formation.is_apc()
]
)
return ", ".join(f"S{indice}" for indice in indices)
def pvjury_table_but(formsemestre_id: int, format="html") -> list[dict]:
"""Page récapitulant les décisions de jury BUT
formsemestre peut être pair ou impair
"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
assert formsemestre.formation.is_apc()
title = "Procès-verbal de jury BUT annuel"
if format == "html":
line_sep = "<br/>"
else:
line_sep = "\n"
# remplace pour le BUT la fonction sco_pvjury.pvjury_table
annee_but = (formsemestre.semestre_id + 1) // 2
titles = {
"nom": "Nom",
"cursus": "Cursus",
"ues": "UE validées",
"niveaux": "Niveaux de compétences validés",
"decision_but": f"Décision BUT{annee_but}",
"diplome": "Résultat au diplôme",
"devenir": "Devenir",
"observations": "Observations",
}
rows = []
for etudid in formsemestre.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid)
try:
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if deca.annee_but != annee_but: # wtf ?
log(
f"pvjury_table_but: inconsistent annee_but {deca.annee_but} != {annee_but}"
)
continue
except ScoValueError:
deca = None
row = {
"nom": etud.etat_civil_pv(line_sep=line_sep),
"_nom_order": etud.sort_key,
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
"_nom_target": url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
),
"cursus": _descr_cursus_but(etud),
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
if deca
else "-",
"decision_but": deca.code_valide if deca else "",
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()]),
}
rows.append(row)
rows.sort(key=lambda x: x["_nom_order"])
# Style excel... passages à la ligne sur \n
xls_style_base = sco_excel.excel_make_style()
xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top")
tab = GenTable(
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
caption=title,
columns_ids=titles.keys(),
html_caption=title,
html_class="pvjury_table_but table_leftalign",
html_title=f"""<div style="margin-bottom: 8px;"><span style="font-size: 120%; font-weight: bold;">{title}</span>
<span style="padding-left: 20px;">
<a href="{url_for("notes.pvjury_table_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, format="xlsx")}"
class="stdlink">version excel</a></span></div>
""",
html_with_td_classes=True,
origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
page_title=title,
pdf_title=title,
preferences=sco_preferences.SemPreferences(),
rows=rows,
table_id="formation_table_recap",
titles=titles,
xls_columns_width={
"nom": 32,
"cursus": 12,
"ues": 32,
"niveaux": 32,
"decision_but": 14,
"diplome": 17,
"devenir": 8,
"observations": 12,
},
xls_style_base=xls_style_base,
)
return tab.make_page(format=format, javascripts=["js/etud_info.js"], init_qtip=True)

View File

@ -36,7 +36,7 @@ from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_saisie_jury_but( def formsemestre_saisie_jury_but(
formsemestre2: FormSemestre, formsemestre2: FormSemestre,
readonly: bool = False, read_only: bool = False,
selected_etudid: int = None, selected_etudid: int = None,
mode="jury", mode="jury",
) -> str: ) -> str:
@ -72,7 +72,7 @@ def formsemestre_saisie_jury_but(
) )
rows, titles, column_ids = get_table_jury_but( rows, titles, column_ids = get_table_jury_but(
formsemestre2, readonly=readonly, mode=mode formsemestre2, read_only=read_only, mode=mode
) )
if not rows: if not rows:
return ( return (
@ -98,7 +98,16 @@ def formsemestre_saisie_jury_but(
] ]
if mode == "recap": if mode == "recap":
H.append( H.append(
"""<h3>Décisions de jury enregistrées pour les étudiants de ce semestre</h3>""" f"""<h3>Décisions de jury enregistrées pour les étudiants de ce semestre</h3>
<div class="table_jury_but_links">
<div>
<a href="{url_for(
"notes.pvjury_table_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}" class="stdlink">tableau PV de jury</a>
</div>
</div>
"""
) )
H.append( H.append(
f""" f"""
@ -109,7 +118,7 @@ def formsemestre_saisie_jury_but(
""" """
) )
if (mode == "recap") and not readonly: if (mode == "recap") and not read_only:
H.append( H.append(
f""" f"""
<p><a class="stdlink" href="{url_for( <p><a class="stdlink" href="{url_for(
@ -333,6 +342,10 @@ class RowCollector:
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""), + ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
"col_rcue col_rcues_validables" + klass, "col_rcue col_rcues_validables" + klass,
) )
self["_rcues_validables_data"] = {
"etudid": deca.etud.id,
"nomprenom": deca.etud.nomprenom,
}
if len(deca.rcues_annee) > 0: if len(deca.rcues_annee) > 0:
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair # permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen: if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
@ -353,7 +366,7 @@ class RowCollector:
def get_table_jury_but( def get_table_jury_but(
formsemestre2: FormSemestre, readonly: bool = False, mode="jury" formsemestre2: FormSemestre, read_only: bool = False, mode="jury"
) -> tuple[list[dict], list[str], list[str]]: ) -> tuple[list[dict], list[str], list[str]]:
"""Construit la table des résultats annuels pour le jury BUT""" """Construit la table des résultats annuels pour le jury BUT"""
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2) res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
@ -383,7 +396,7 @@ def get_table_jury_but(
"col_code_annee", "col_code_annee",
) )
# --- Le lien de saisie # --- Le lien de saisie
if not readonly and not mode == "recap": if mode != "recap":
row.add_cell( row.add_cell(
"lien_saisie", "lien_saisie",
"", "",
@ -394,9 +407,11 @@ def get_table_jury_but(
etudid=etud.id, etudid=etud.id,
formsemestre_id=formsemestre2.id, formsemestre_id=formsemestre2.id,
)}" class="stdlink"> )}" class="stdlink">
{"modif." if deca.code_valide else "saisie"} {"voir" if read_only else ("modif." if deca.code_valide else "saisie")}
décision</a> décision</a>
""", """
if deca.inscription_etat == scu.INSCRIT
else deca.inscription_etat,
"col_lien_saisie_but", "col_lien_saisie_but",
) )
rows.append(row) rows.append(row)

173
app/but/jury_but_view.py Normal file
View File

@ -0,0 +1,173 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Jury BUT: affichage/formulaire
"""
from flask import g, url_for
from app.models.etudiants import Identite
from app.scodoc import sco_utils as scu
from app.but.jury_but import DecisionsProposeesAnnee, DecisionsProposeesUE
from app.models import FormSemestre, FormSemestreInscription, UniteEns
from app.scodoc.sco_exceptions import ScoValueError
def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
"""Affichage des décisions annuelles BUT
Si pas read_only, menus sélection codes jury.
"""
H = []
if deca.code_valide and not read_only:
erase_span = f"""<a href="{
url_for("notes.formsemestre_jury_but_erase",
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id,
etudid=deca.etud.id)}" class="stdlink">effacer décisions</a>"""
else:
erase_span = ""
H.append(
f"""<div class="but_section_annee">
<div>
<b>Décision de jury pour l'année :</b> {
_gen_but_select("code_annee", deca.codes, deca.code_valide,
disabled=True, klass="manual")
}
<span>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
<span>{erase_span}</span>
</div>
<div class="but_explanation">{deca.explanation}</div>
</div>"""
)
H.append(
f"""
<div><b>Niveaux de compétences et unités d'enseignement :</b></div>
<div class="but_annee">
<div class="titre"></div>
<div class="titre">S{1}</div>
<div class="titre">S{2}</div>
<div class="titre">RCUE</div>
"""
)
for niveau in deca.niveaux_competences:
H.append(
f"""<div class="but_niveau_titre">
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
</div>"""
)
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id)
if dec_rcue is None:
break
# Semestre impair
H.append(
_gen_but_niveau_ue(
dec_rcue.rcue.ue_1,
dec_rcue.rcue.moy_ue_1,
deca.decisions_ues[dec_rcue.rcue.ue_1.id],
disabled=read_only,
)
)
# Semestre pair
H.append(
_gen_but_niveau_ue(
dec_rcue.rcue.ue_2,
dec_rcue.rcue.moy_ue_2,
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
disabled=read_only,
)
)
# RCUE
H.append(
f"""<div class="but_niveau_rcue
{'recorded' if dec_rcue.code_valide is not None else ''}
">
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
<div class="but_code">{
_gen_but_select("code_rcue_"+str(niveau.id),
dec_rcue.codes,
dec_rcue.code_valide,
disabled=True, klass="manual"
)
}</div>
</div>"""
)
H.append("</div>") # but_annee
return "\n".join(H)
def _gen_but_select(
name: str,
codes: list[str],
code_valide: str,
disabled: bool = False,
klass: str = "",
) -> str:
"Le menu html select avec les codes"
h = "\n".join(
[
f"""<option value="{code}"
{'selected' if code == code_valide else ''}
class="{'recorded' if code == code_valide else ''}"
>{code}</option>"""
for code in codes
]
)
return f"""<select required name="{name}"
class="but_code {klass}"
onchange="change_menu_code(this);"
{"disabled" if disabled else ""}
>{h}</select>
"""
def _gen_but_niveau_ue(
ue: UniteEns, moy_ue: float, dec_ue: DecisionsProposeesUE, disabled=False
):
return f"""<div class="but_niveau_ue {
'recorded' if dec_ue.code_valide is not None else ''}
">
<div title="{ue.titre}">{ue.acronyme}</div>
<div class="but_note">{scu.fmt_note(moy_ue)}</div>
<div class="but_code">{
_gen_but_select("code_ue_"+str(ue.id),
dec_ue.codes,
dec_ue.code_valide, disabled=disabled
)
}</div>
</div>"""
#
def infos_fiche_etud_html(etudid: int) -> str:
"""Section html pour fiche etudiant
provisoire pour BUT 2022
"""
etud: Identite = Identite.query.get_or_404(etudid)
inscriptions = (
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
.filter(
FormSemestreInscription.etudid == etud.id,
)
.order_by(FormSemestre.date_debut)
)
formsemestres_but = [
i.formsemestre for i in inscriptions if i.formsemestre.formation.is_apc()
]
if len(formsemestres_but) == 0:
return ""
# temporaire quick & dirty: affiche le dernier
try:
deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1])
if len(deca.rcues_annee) > 0:
return f"""<div class="infos_but">
{show_etud(deca, read_only=True)}
</div>
"""
except ScoValueError:
pass
return ""

View File

@ -496,17 +496,26 @@ def compute_malus(
""" """
ues_idx = [ue.id for ue in ues] ues_idx = [ue.id for ue in ues]
malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float) malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float)
if len(sem_modimpl_moys.flat) == 0: # vide
return malus
if len(sem_modimpl_moys.shape) > 2:
# BUT: ne retient que la 1er composante du malus qui est scalaire
# au sens ou chaque note de malus n'affecte que la moyenne de l'UE
# de rattachement de son module.
sem_modimpl_moys_scalar = sem_modimpl_moys[:, :, 0]
else: # classic
sem_modimpl_moys_scalar = sem_modimpl_moys
for ue in ues: for ue in ues:
if ue.type != UE_SPORT: if ue.type != UE_SPORT:
modimpl_mask = np.array( modimpl_mask = np.array(
[ [
(m.module.module_type == ModuleType.MALUS) (m.module.module_type == ModuleType.MALUS)
and (m.module.ue.id == ue.id) and (m.module.ue.id == ue.id) # UE de rattachement
for m in formsemestre.modimpls_sorted for m in formsemestre.modimpls_sorted
] ]
) )
if len(modimpl_mask): if len(modimpl_mask):
malus_moys = sem_modimpl_moys[:, modimpl_mask].sum(axis=1) malus_moys = sem_modimpl_moys_scalar[:, modimpl_mask].sum(axis=1)
malus[ue.id] = malus_moys malus[ue.id] = malus_moys
malus.fillna(0.0, inplace=True) malus.fillna(0.0, inplace=True)

View File

@ -399,7 +399,7 @@ class ResultatsSemestre(ResultatsCache):
# --- TABLEAU RECAP # --- TABLEAU RECAP
def get_table_recap( def get_table_recap(
self, convert_values=False, include_evaluations=False, modejury=False self, convert_values=False, include_evaluations=False, mode_jury=False
): ):
"""Result: tuple avec """Result: tuple avec
- rows: liste de dicts { column_id : value } - rows: liste de dicts { column_id : value }
@ -550,7 +550,7 @@ class ResultatsSemestre(ResultatsCache):
titles_bot[ titles_bot[
f"_{col_id}_target_attrs" f"_{col_id}_target_attrs"
] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """ ] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
if modejury: if mode_jury:
# pas d'autre colonnes de résultats # pas d'autre colonnes de résultats
continue continue
# Bonus (sport) dans cette UE ? # Bonus (sport) dans cette UE ?
@ -650,7 +650,17 @@ class ResultatsSemestre(ResultatsCache):
elif nb_ues_validables < len(ues_sans_bonus): elif nb_ues_validables < len(ues_sans_bonus):
row["_ues_validables_class"] += " moy_inf" row["_ues_validables_class"] += " moy_inf"
row["_ues_validables_order"] = nb_ues_validables # pour tri row["_ues_validables_order"] = nb_ues_validables # pour tri
if modejury: if mode_jury and self.validations:
dec_sem = self.validations.decisions_jury.get(etudid)
jury_code_sem = dec_sem["code"] if dec_sem else ""
idx = add_cell(
row,
"jury_code_sem",
"Jury",
jury_code_sem,
"jury_code_sem",
1000,
)
idx = add_cell( idx = add_cell(
row, row,
"jury_link", "jury_link",
@ -660,7 +670,7 @@ class ResultatsSemestre(ResultatsCache):
) )
}">saisir décision</a>""", }">saisir décision</a>""",
"col_jury_link", "col_jury_link",
1000, idx,
) )
rows.append(row) rows.append(row)

View File

@ -54,6 +54,7 @@ class NotesTableCompat(ResultatsSemestre):
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}} self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
self.expr_diagnostics = "" self.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_parcours() self.parcours = self.formsemestre.formation.get_parcours()
self._modimpls_dict_by_ue = {} # local cache
def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]: def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
"""Liste des étudiants inscrits """Liste des étudiants inscrits
@ -145,6 +146,10 @@ class NotesTableCompat(ResultatsSemestre):
"""Liste des modules pour une UE (ou toutes si ue_id==None), """Liste des modules pour une UE (ou toutes si ue_id==None),
triés par numéros (selon le type de formation) triés par numéros (selon le type de formation)
""" """
# cached ?
modimpls_dict = self._modimpls_dict_by_ue.get(ue_id)
if modimpls_dict:
return modimpls_dict
modimpls_dict = [] modimpls_dict = []
for modimpl in self.formsemestre.modimpls_sorted: for modimpl in self.formsemestre.modimpls_sorted:
if (ue_id is None) or (modimpl.module.ue.id == ue_id): if (ue_id is None) or (modimpl.module.ue.id == ue_id):
@ -152,6 +157,7 @@ class NotesTableCompat(ResultatsSemestre):
# compat ScoDoc < 9.2: ajoute matières # compat ScoDoc < 9.2: ajoute matières
d["mat"] = modimpl.module.matiere.to_dict() d["mat"] = modimpl.module.matiere.to_dict()
modimpls_dict.append(d) modimpls_dict.append(d)
self._modimpls_dict_by_ue[ue_id] = modimpls_dict
return modimpls_dict return modimpls_dict
def compute_rangs(self): def compute_rangs(self):

View File

@ -43,7 +43,7 @@ from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import LogoInsert from app.scodoc.sco_config_actions import LogoInsert
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_logos import find_logo from app.scodoc.sco_logos import find_logo
@ -108,6 +108,8 @@ def dept_key_to_id(dept_key):
def logo_name_validator(message=None): def logo_name_validator(message=None):
def validate_logo_name(form, field): def validate_logo_name(form, field):
name = field.data if field.data else "" name = field.data if field.data else ""
if "." in name:
raise ValidationError(message)
if not scu.is_valid_filename(name): if not scu.is_valid_filename(name):
raise ValidationError(message) raise ValidationError(message)
@ -199,9 +201,12 @@ class LogoForm(FlaskForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs["meta"] = {"csrf": False} kwargs["meta"] = {"csrf": False}
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.logo = find_logo( logo = find_logo(
logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data) logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data)
).select() )
if logo is None:
raise ScoValueError("logo introuvable")
self.logo = logo.select()
self.description = None self.description = None
self.titre = None self.titre = None
self.can_delete = True self.can_delete = True

View File

@ -255,6 +255,7 @@ class ApcCompetence(db.Model, XMLModel):
return f"<ApcCompetence {self.id} {self.titre!r}>" return f"<ApcCompetence {self.id} {self.titre!r}>"
def to_dict(self): def to_dict(self):
"repr dict recursive sur situations, composantes, niveaux"
return { return {
"id_orebut": self.id_orebut, "id_orebut": self.id_orebut,
"titre": self.titre, "titre": self.titre,
@ -268,6 +269,16 @@ class ApcCompetence(db.Model, XMLModel):
"niveaux": {x.annee: x.to_dict() for x in self.niveaux}, "niveaux": {x.annee: x.to_dict() for x in self.niveaux},
} }
def to_dict_bul(self) -> dict:
"dict court pour bulletins"
return {
"id_orebut": self.id_orebut,
"titre": self.titre,
"titre_long": self.titre_long,
"couleur": self.couleur,
"numero": self.numero,
}
class ApcSituationPro(db.Model, XMLModel): class ApcSituationPro(db.Model, XMLModel):
"Situation professionnelle" "Situation professionnelle"
@ -341,6 +352,7 @@ class ApcNiveau(db.Model, XMLModel):
self.annee!r} {self.competence!r}>""" self.annee!r} {self.competence!r}>"""
def to_dict(self): def to_dict(self):
"as a dict, recursif sur les AC"
return { return {
"libelle": self.libelle, "libelle": self.libelle,
"annee": self.annee, "annee": self.annee,
@ -348,6 +360,15 @@ class ApcNiveau(db.Model, XMLModel):
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}, "app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
} }
def to_dict_bul(self):
"dict pour bulletins: indique la compétence, pas les ACs (pour l'instant ?)"
return {
"libelle": self.libelle,
"annee": self.annee,
"ordre": self.ordre,
"competence": self.competence.to_dict_bul(),
}
@classmethod @classmethod
def niveaux_annee_de_parcours( def niveaux_annee_de_parcours(
cls, cls,
@ -430,6 +451,7 @@ class ApcAppCritique(db.Model, XMLModel):
if competence is not None: if competence is not None:
query = query.filter(ApcNiveau.competence == competence) query = query.filter(ApcNiveau.competence == competence)
return query return query
<<<<<<< HEAD
def __init__(self, id, niveau_id, code, libelle, modules): def __init__(self, id, niveau_id, code, libelle, modules):
self.id = id self.id = id
@ -437,6 +459,8 @@ class ApcAppCritique(db.Model, XMLModel):
self.code = code self.code = code
self.libelle = libelle self.libelle = libelle
self.modules = modules self.modules = modules
=======
>>>>>>> 7c340c798ad59c41653efc83bfd079f11fce1938
def to_dict(self) -> dict: def to_dict(self) -> dict:
return {"libelle": self.libelle} return {"libelle": self.libelle}
@ -523,11 +547,14 @@ class ApcAnneeParcours(db.Model, XMLModel):
) )
ordre = db.Column(db.Integer) ordre = db.Column(db.Integer)
"numéro de l'année: 1, 2, 3" "numéro de l'année: 1, 2, 3"
<<<<<<< HEAD
def __init__(self, id, parcours_id, ordre): def __init__(self, id, parcours_id, ordre):
self.id = id self.id = id
self.parcours_id = parcours_id self.parcours_id = parcours_id
self.ordre = ordre self.ordre = ordre
=======
>>>>>>> 7c340c798ad59c41653efc83bfd079f11fce1938
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} ordre={self.ordre!r} parcours={self.parcours.code!r}>" return f"<{self.__class__.__name__} ordre={self.ordre!r} parcours={self.parcours.code!r}>"

View File

@ -13,8 +13,10 @@ from app.models import CODE_STR_LEN
from app.models.but_refcomp import ApcNiveau from app.models.but_refcomp import ApcNiveau
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours as sco_codes from app.scodoc import sco_codes_parcours as sco_codes
from app.scodoc import sco_utils as scu
class ApcValidationRCUE(db.Model): class ApcValidationRCUE(db.Model):
@ -41,6 +43,7 @@ class ApcValidationRCUE(db.Model):
formsemestre_id = db.Column( formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
) )
"formsemestre pair du RCUE"
# Les deux UE associées à ce niveau: # Les deux UE associées à ce niveau:
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
@ -63,6 +66,10 @@ class ApcValidationRCUE(db.Model):
# Par convention, il est donné par la seconde UE # Par convention, il est donné par la seconde UE
return self.ue2.niveau_competence return self.ue2.niveau_competence
def to_dict_bul(self) -> dict:
"Export dict pour bulletins"
return {"code": self.code, "niveau": self.niveau().to_dict_bul()}
# Attention: ce n'est pas un modèle mais une classe ordinaire: # Attention: ce n'est pas un modèle mais une classe ordinaire:
class RegroupementCoherentUE: class RegroupementCoherentUE:
@ -79,6 +86,7 @@ class RegroupementCoherentUE:
ue_1: UniteEns, ue_1: UniteEns,
formsemestre_2: FormSemestre, formsemestre_2: FormSemestre,
ue_2: UniteEns, ue_2: UniteEns,
inscription_etat: str,
): ):
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
@ -104,6 +112,11 @@ class RegroupementCoherentUE:
"semestre pair" "semestre pair"
self.ue_2 = ue_2 self.ue_2 = ue_2
# Stocke les moyennes d'UE # Stocke les moyennes d'UE
if inscription_etat != scu.INSCRIT:
self.moy_rcue = None
self.moy_ue_1 = self.moy_ue_2 = "-"
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
return
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]: if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id] self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
@ -190,14 +203,15 @@ class RegroupementCoherentUE:
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None" "Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
validation = self.query_validations().first() validation = self.query_validations().first()
if (validation is not None) and ( if (validation is not None) and (
validation.code in {sco_codes.ADM, sco_codes.ADJ, sco_codes.CMP} validation.code in sco_codes.CODES_RCUE_VALIDES
): ):
return validation return validation
return None return None
# unused
def find_rcues( def find_rcues(
formsemestre: FormSemestre, ue: UniteEns, etud: Identite formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
) -> list[RegroupementCoherentUE]: ) -> list[RegroupementCoherentUE]:
"""Les RCUE (niveau de compétence) à considérer pour cet étudiant dans """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
ce semestre pour cette UE. ce semestre pour cette UE.
@ -245,7 +259,9 @@ def find_rcues(
other_ue = UniteEns.query.get(ue_id) other_ue = UniteEns.query.get(ue_id)
other_formsemestre = FormSemestre.query.get(formsemestre_id) other_formsemestre = FormSemestre.query.get(formsemestre_id)
rcues.append( rcues.append(
RegroupementCoherentUE(etud, formsemestre, ue, other_formsemestre, other_ue) RegroupementCoherentUE(
etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
)
) )
# safety check: 1 seul niveau de comp. concerné: # safety check: 1 seul niveau de comp. concerné:
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1 assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
@ -280,3 +296,45 @@ class ApcValidationAnnee(db.Model):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>" return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"
def to_dict_bul(self) -> dict:
"dict pour bulletins"
return {
"annee_scolaire": self.annee_scolaire,
"date": self.date.isoformat(),
"code": self.code,
"ordre": self.ordre,
}
def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
"""
Un dict avec les décisions de jury BUT enregistrées.
Ne reprend pas les décisions d'UE, non spécifiques au BUT.
"""
decisions = {}
# --- RCUEs: seulement sur semestres pairs XXX à améliorer
if formsemestre.semestre_id % 2 == 0:
# validations émises depuis ce formsemestre:
validations_rcues = ApcValidationRCUE.query.filter_by(
etudid=etud.id, formsemestre_id=formsemestre.id
)
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
else:
decisions["decision_rcue"] = []
# --- Année: prend la validation pour l'année scolaire de ce semestre
validation = (
ApcValidationAnnee.query.filter_by(
etudid=etud.id,
annee_scolaire=formsemestre.annee_scolaire(),
)
.join(ApcValidationAnnee.formsemestre)
.join(FormSemestre.formation)
.filter(Formation.formation_code == formsemestre.formation.formation_code)
.first()
)
if validation:
decisions["decision_annee"] = validation.to_dict_bul()
else:
decisions["decision_annee"] = None
return decisions

View File

@ -26,6 +26,7 @@ from app.scodoc.sco_codes_parcours import (
PASD, PASD,
PAS1NCI, PAS1NCI,
RAT, RAT,
RED,
) )
CODES_SCODOC_TO_APO = { CODES_SCODOC_TO_APO = {
@ -46,6 +47,7 @@ CODES_SCODOC_TO_APO = {
PASD: "PASD", PASD: "PASD",
PAS1NCI: "PAS1NCI", PAS1NCI: "PAS1NCI",
RAT: "ATT", RAT: "ATT",
RED: "RED",
"NOTES_FMT": "%3.2f", "NOTES_FMT": "%3.2f",
} }

View File

@ -136,9 +136,9 @@ class Identite(db.Model):
"clé pour tris par ordre alphabétique" "clé pour tris par ordre alphabétique"
return ( return (
scu.sanitize_string( scu.sanitize_string(
scu.suppress_accents(self.nom_usuel or self.nom or "").lower() self.nom_usuel or self.nom or "", remove_spaces=False
), ).lower(),
scu.sanitize_string(scu.suppress_accents(self.prenom or "").lower()), scu.sanitize_string(self.prenom or "", remove_spaces=False).lower(),
) )
def get_first_email(self, field="email") -> str: def get_first_email(self, field="email") -> str:
@ -205,6 +205,19 @@ class Identite(db.Model):
d.update(adresse.to_dict(convert_nulls_to_str=True)) d.update(adresse.to_dict(convert_nulls_to_str=True))
return d return d
def inscriptions(self) -> list["FormSemestreInscription"]:
"Liste des inscriptions à des formsemestres, triée, la plus récente en tête"
from app.models.formsemestre import FormSemestre, FormSemestreInscription
return (
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
.filter(
FormSemestreInscription.etudid == self.id,
)
.order_by(desc(FormSemestre.date_debut))
.all()
)
def inscription_courante(self): def inscription_courante(self):
"""La première inscription à un formsemestre _actuellement_ en cours. """La première inscription à un formsemestre _actuellement_ en cours.
None s'il n'y en a pas (ou plus, ou pas encore). None s'il n'y en a pas (ou plus, ou pas encore).
@ -216,7 +229,7 @@ class Identite(db.Model):
] ]
return r[0] if r else None return r[0] if r else None
def inscriptions_courantes(self) -> list: # -> list[FormSemestreInscription]: def inscriptions_courantes(self) -> list["FormSemestreInscription"]:
"""Liste des inscriptions à des semestres _courants_ """Liste des inscriptions à des semestres _courants_
(il est rare qu'il y en ai plus d'une, mais c'est possible). (il est rare qu'il y en ai plus d'une, mais c'est possible).
Triées par date de début de semestre décroissante (le plus récent en premier). Triées par date de début de semestre décroissante (le plus récent en premier).
@ -244,18 +257,6 @@ class Identite(db.Model):
] ]
return r[0] if r else None return r[0] if r else None
def inscription_etat(self, formsemestre_id):
"""État de l'inscription de cet étudiant au semestre:
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
"""
# voir si ce n'est pas trop lent:
ins = models.FormSemestreInscription.query.filter_by(
etudid=self.id, formsemestre_id=formsemestre_id
).first()
if ins:
return ins.etat
return False
def inscription_descr(self) -> dict: def inscription_descr(self) -> dict:
"""Description de l'état d'inscription""" """Description de l'état d'inscription"""
inscription_courante = self.inscription_courante() inscription_courante = self.inscription_courante()
@ -294,6 +295,18 @@ class Identite(db.Model):
"situation": situation, "situation": situation,
} }
def inscription_etat(self, formsemestre_id):
"""État de l'inscription de cet étudiant au semestre:
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
"""
# voir si ce n'est pas trop lent:
ins = models.FormSemestreInscription.query.filter_by(
etudid=self.id, formsemestre_id=formsemestre_id
).first()
if ins:
return ins.etat
return False
def descr_situation_etud(self) -> str: def descr_situation_etud(self) -> str:
"""Chaîne décrivant la situation _actuelle_ de l'étudiant. """Chaîne décrivant la situation _actuelle_ de l'étudiant.
Exemple: Exemple:
@ -365,6 +378,15 @@ class Identite(db.Model):
return situation return situation
def etat_civil_pv(self, line_sep="\n") -> str:
"""Présentation, pour PV jury
M. Pierre Dupont
n° 12345678
(e) le 7/06/1974
à Paris
"""
return f"""{self.nomprenom}{line_sep}{self.code_nip or ""}{line_sep}{self.e} le {self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{line_sep}à {self.lieu_naissance or ""}"""
def photo_html(self, title=None, size="small") -> str: def photo_html(self, title=None, size="small") -> str:
"""HTML img tag for the photo, either in small size (h90) """HTML img tag for the photo, either in small size (h90)
or original size (size=="orig") or original size (size=="orig")

View File

@ -141,7 +141,7 @@ class FormSemestre(db.Model):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>" return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
def to_dict(self): def to_dict(self, convert_parcours=False):
"dict (compatible ScoDoc7)" "dict (compatible ScoDoc7)"
d = dict(self.__dict__) d = dict(self.__dict__)
d.pop("_sa_instance_state", None) d.pop("_sa_instance_state", None)
@ -160,6 +160,8 @@ class FormSemestre(db.Model):
d["date_fin"] = d["date_fin_iso"] = "" d["date_fin"] = d["date_fin_iso"] = ""
d["responsables"] = [u.id for u in self.responsables] d["responsables"] = [u.id for u in self.responsables]
d["titre_formation"] = self.titre_formation() d["titre_formation"] = self.titre_formation()
if convert_parcours:
d["parcours"] = [p.to_dict() for p in self.parcours]
return d return d
def to_dict_api(self): def to_dict_api(self):
@ -507,6 +509,19 @@ class FormSemestre(db.Model):
etudid, self.date_debut.isoformat(), self.date_fin.isoformat() etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
) )
def get_codes_apogee(self, category=None) -> set[str]:
"""Les codes Apogée (codés en base comme "VRT1,VRT2")
category: None: tous, "etapes": étapes associées, "sem: code semestre", "annee": code annuel
"""
codes = set()
if category is None or category == "etapes":
codes |= {e.etape_apo for e in self.etapes if e}
if (category is None or category == "sem") and self.elt_sem_apo:
codes |= {x.strip() for x in self.elt_sem_apo.split(",") if x}
if (category is None or category == "annee") and self.elt_annee_apo:
codes |= {x.strip() for x in self.elt_annee_apo.split(",") if x}
return codes
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]: def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
"""Liste des étudiants inscrits à ce semestre """Liste des étudiants inscrits à ce semestre
Si include_demdef, tous les étudiants, avec les démissionnaires Si include_demdef, tous les étudiants, avec les démissionnaires

View File

@ -175,6 +175,12 @@ class Module(db.Model):
# Liste seulement les coefs définis: # Liste seulement les coefs définis:
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()] return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
def get_codes_apogee(self) -> set[str]:
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
if self.code_apogee:
return {x.strip() for x in self.code_apogee.split(",") if x}
return set()
class ModuleUECoef(db.Model): class ModuleUECoef(db.Model):
"""Coefficients des modules vers les UE (APC, BUT) """Coefficients des modules vers les UE (APC, BUT)

View File

@ -120,3 +120,9 @@ class UniteEns(db.Model):
(Module.module_type != scu.ModuleType.SAE), (Module.module_type != scu.ModuleType.SAE),
(Module.module_type != scu.ModuleType.RESSOURCE), (Module.module_type != scu.ModuleType.RESSOURCE),
).all() ).all()
def get_codes_apogee(self) -> set[str]:
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
if self.code_apogee:
return {x.strip() for x in self.code_apogee.split(",") if x}
return set()

View File

@ -45,7 +45,7 @@ import random
from collections import OrderedDict from collections import OrderedDict
from xml.etree import ElementTree from xml.etree import ElementTree
import json import json
from openpyxl.utils import get_column_letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from reportlab.lib.colors import Color from reportlab.lib.colors import Color
@ -127,6 +127,8 @@ class GenTable(object):
filename="table", # filename, without extension filename="table", # filename, without extension
xls_sheet_name="feuille", xls_sheet_name="feuille",
xls_before_table=[], # liste de cellules a placer avant la table xls_before_table=[], # liste de cellules a placer avant la table
xls_style_base=None, # style excel pour les cellules
xls_columns_width=None, # { col_id : largeur en "pixels excel" }
pdf_title="", # au dessus du tableau en pdf pdf_title="", # au dessus du tableau en pdf
pdf_table_style=None, pdf_table_style=None,
pdf_col_widths=None, pdf_col_widths=None,
@ -151,6 +153,8 @@ class GenTable(object):
self.page_title = page_title self.page_title = page_title
self.pdf_link = pdf_link self.pdf_link = pdf_link
self.xls_link = xls_link self.xls_link = xls_link
self.xls_style_base = xls_style_base
self.xls_columns_width = xls_columns_width or {}
self.xml_link = xml_link self.xml_link = xml_link
# HTML parameters: # HTML parameters:
if not table_id: # random id if not table_id: # random id
@ -495,7 +499,8 @@ class GenTable(object):
sheet = wb.create_sheet(sheet_name=self.xls_sheet_name) sheet = wb.create_sheet(sheet_name=self.xls_sheet_name)
sheet.rows += self.xls_before_table sheet.rows += self.xls_before_table
style_bold = sco_excel.excel_make_style(bold=True) style_bold = sco_excel.excel_make_style(bold=True)
style_base = sco_excel.excel_make_style() style_base = self.xls_style_base or sco_excel.excel_make_style()
sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold)) sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold))
for line in self.get_data_list(xls_mode=True): for line in self.get_data_list(xls_mode=True):
sheet.append_row(sheet.make_row(line, style_base)) sheet.append_row(sheet.make_row(line, style_base))
@ -505,6 +510,16 @@ class GenTable(object):
if self.origin: if self.origin:
sheet.append_blank_row() # empty line sheet.append_blank_row() # empty line
sheet.append_single_cell_row(self.origin, style_base) sheet.append_single_cell_row(self.origin, style_base)
# Largeurs des colonnes
columns_ids = list(self.columns_ids)
for col_id, width in self.xls_columns_width.items():
try:
idx = columns_ids.index(col_id)
col = get_column_letter(idx + 1)
sheet.set_column_dimension_width(col, width)
except ValueError:
pass
if wb is None: if wb is None:
return sheet.generate() return sheet.generate()

View File

@ -258,11 +258,16 @@ class ApoEtud(dict):
self["nom"] = nom self["nom"] = nom
self["prenom"] = prenom self["prenom"] = prenom
self["naissance"] = naissance self["naissance"] = naissance
self.cols = cols # { col_id : value } colid = 'apoL_c0001' self.cols = cols
"{ col_id : value } colid = 'apoL_c0001'"
self.col_elts = {}
"{'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}"
self.new_cols = {} # { col_id : value to record in csv } self.new_cols = {} # { col_id : value to record in csv }
self.etud = None # etud ScoDoc self.etud: Identite = None
"etudiant ScoDoc associé"
self.etat = None # ETUD_OK, ... self.etat = None # ETUD_OK, ...
self.is_NAR = False # set to True si NARé dans un semestre self.is_NAR = False
"True si NARé dans un semestre"
self.log = [] self.log = []
self.has_logged_no_decision = False self.has_logged_no_decision = False
self.export_res_etape = export_res_etape # VET, ... self.export_res_etape = export_res_etape # VET, ...
@ -276,7 +281,7 @@ class ApoEtud(dict):
) )
def __repr__(self): def __repr__(self):
return "ApoEtud( nom='%s', nip='%s' )" % (self["nom"], self["nip"]) return f"""ApoEtud( nom='{self["nom"]}', nip='{self["nip"]}' )"""
def lookup_scodoc(self, etape_formsemestre_ids): def lookup_scodoc(self, etape_formsemestre_ids):
"""Cherche l'étudiant ScoDoc associé à cet étudiant Apogée. """Cherche l'étudiant ScoDoc associé à cet étudiant Apogée.
@ -284,6 +289,10 @@ class ApoEtud(dict):
met .etud à None. met .etud à None.
Sinon, cherche le semestre, et met l'état à ETUD_OK ou ETUD_NON_INSCRIT. Sinon, cherche le semestre, et met l'état à ETUD_OK ou ETUD_NON_INSCRIT.
""" """
# futur: #WIP
# etud: Identite = Identite.query.filter_by(code_nip=self["nip"]).first()
# self.etud = etud
etuds = sco_etud.get_etud_info(code_nip=self["nip"], filled=True) etuds = sco_etud.get_etud_info(code_nip=self["nip"], filled=True)
if not etuds: if not etuds:
# pas dans ScoDoc # pas dans ScoDoc
@ -291,13 +300,16 @@ class ApoEtud(dict):
self.log.append("non inscrit dans ScoDoc") self.log.append("non inscrit dans ScoDoc")
self.etat = ETUD_ORPHELIN self.etat = ETUD_ORPHELIN
else: else:
# futur: #WIP
# formsemestre_ids = {
# ins.formsemestre_id for ins in etud.formsemestre_inscriptions
# }
# in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
self.etud = etuds[0] self.etud = etuds[0]
# cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape: # cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape:
formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]} formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]}
self.in_formsemestre_ids = formsemestre_ids.intersection( in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
etape_formsemestre_ids if not in_formsemestre_ids:
)
if not self.in_formsemestre_ids:
self.log.append( self.log.append(
"connu dans ScoDoc, mais pas inscrit dans un semestre de cette étape" "connu dans ScoDoc, mais pas inscrit dans un semestre de cette étape"
) )
@ -305,7 +317,7 @@ class ApoEtud(dict):
else: else:
self.etat = ETUD_OK self.etat = ETUD_OK
def associate_sco(self, apo_data): def associate_sco(self, apo_data: "ApoData"):
"""Recherche les valeurs des éléments Apogée pour cet étudiant """Recherche les valeurs des éléments Apogée pour cet étudiant
Set .new_cols Set .new_cols
""" """
@ -327,7 +339,7 @@ class ApoEtud(dict):
cur_sem, autre_sem = self.etud_semestres_de_etape(apo_data) cur_sem, autre_sem = self.etud_semestres_de_etape(apo_data)
for sem in apo_data.sems_etape: for sem in apo_data.sems_etape:
el = self.search_elt_in_sem(code, sem, cur_sem, autre_sem) el = self.search_elt_in_sem(code, sem, cur_sem, autre_sem)
if el != None: if el is not None:
sco_elts[code] = el sco_elts[code] = el
break break
self.col_elts[code] = el self.col_elts[code] = el
@ -338,15 +350,15 @@ class ApoEtud(dict):
self.new_cols[col_id] = sco_elts[code][ self.new_cols[col_id] = sco_elts[code][
apo_data.cols[col_id]["Type Rés."] apo_data.cols[col_id]["Type Rés."]
] ]
except KeyError: except KeyError as exc:
log( log(
"associate_sco: missing key, etud=%s\ncode='%s'\netape='%s'" f"associate_sco: missing key, etud={self}\ncode='{code}'\netape='{apo_data.etape_apogee}'"
% (self, code, apo_data.etape_apogee)
) )
raise ScoValueError( raise ScoValueError(
"""L'élément %s n'a pas de résultat: peut-être une erreur dans les codes sur le programme pédagogique (vérifier qu'il est bien associé à une UE ou semestre)?""" f"""L'élément {code} n'a pas de résultat: peut-être une erreur
% code dans les codes sur le programme pédagogique
) (vérifier qu'il est bien associé à une UE ou semestre)?"""
) from exc
# recopie les 4 premieres colonnes (nom, ..., naissance): # recopie les 4 premieres colonnes (nom, ..., naissance):
for col_id in apo_data.col_ids[:4]: for col_id in apo_data.col_ids[:4]:
self.new_cols[col_id] = self.cols[col_id] self.new_cols[col_id] = self.cols[col_id]
@ -356,7 +368,7 @@ class ApoEtud(dict):
# codes = set([apo_data.cols[col_id].code for col_id in apo_data.col_ids]) # codes = set([apo_data.cols[col_id].code for col_id in apo_data.col_ids])
# return codes - set(sco_elts) # return codes - set(sco_elts)
def search_elt_in_sem(self, code, sem, cur_sem, autre_sem): def search_elt_in_sem(self, code, sem, cur_sem, autre_sem) -> dict:
""" """
VET code jury etape VET code jury etape
ELP élément pédagogique: UE, module ELP élément pédagogique: UE, module
@ -820,10 +832,8 @@ class ApoData(object):
elts[col["Code"]] = ApoElt([col]) elts[col["Code"]] = ApoElt([col])
return elts # { code apo : ApoElt } return elts # { code apo : ApoElt }
def apo_read_etuds(self, f): def apo_read_etuds(self, f) -> list[ApoEtud]:
"""Lecture des etudiants (et resultats) du fichier CSV Apogée """Lecture des etudiants (et resultats) du fichier CSV Apogée"""
-> liste de dicts
"""
L = [] L = []
while True: while True:
line = f.readline() line = f.readline()
@ -958,36 +968,38 @@ class ApoData(object):
""" """
codes_by_sem = {} codes_by_sem = {}
for sem in self.sems_etape: for sem in self.sems_etape:
formsemestre: FormSemestre = FormSemestre.query.get_or_404(
sem["formsemestre_id"]
)
# L'ensemble des codes apo associés aux éléments:
codes_semestre = formsemestre.get_codes_apogee()
codes_modules = set().union(
*[
modimpl.module.get_codes_apogee()
for modimpl in formsemestre.modimpls
]
)
codes_ues = set().union(
*[
ue.get_codes_apogee()
for ue in formsemestre.query_ues(with_sport=True)
]
)
s = set() s = set()
codes_by_sem[sem["formsemestre_id"]] = s codes_by_sem[sem["formsemestre_id"]] = s
for col_id in self.col_ids[4:]: for col_id in self.col_ids[4:]:
code = self.cols[col_id]["Code"] # 'V1RT' code = self.cols[col_id]["Code"] # 'V1RT'
# associé à l'étape, l'année ou les semestre: # associé à l'étape, l'année ou le semestre:
if ( if code in codes_semestre:
sco_formsemestre.sem_has_etape(sem, code)
or (code in {x.strip() for x in sem["elt_sem_apo"].split(",")})
or (code in {x.strip() for x in sem["elt_annee_apo"].split(",")})
):
s.add(code) s.add(code)
continue continue
# associé à une UE: # associé à une UE:
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) if code in codes_ues:
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) s.add(code)
for ue in nt.get_ues_stat_dict(): continue
if ue["code_apogee"]:
codes = {x.strip() for x in ue["code_apogee"].split(",")}
if code in codes:
s.add(code)
continue
# associé à un module: # associé à un module:
modimpls = nt.get_modimpls_dict() if code in codes_modules:
for modimpl in modimpls: s.add(code)
module = modimpl["module"]
if module["code_apogee"]:
codes = {x.strip() for x in module["code_apogee"].split(",")}
if code in codes:
s.add(code)
continue
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem)) # log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
return codes_by_sem return codes_by_sem

View File

@ -47,6 +47,7 @@
qui est une description (humaine, format libre) de l'archive. qui est une description (humaine, format libre) de l'archive.
""" """
import chardet
import datetime import datetime
import glob import glob
import json import json
@ -55,7 +56,7 @@ import os
import re import re
import shutil import shutil
import time import time
import chardet from typing import Union
import flask import flask
from flask import g, request from flask import g, request
@ -232,14 +233,17 @@ class BaseArchiver(object):
os.mkdir(archive_id) # if exists, raises an OSError os.mkdir(archive_id) # if exists, raises an OSError
finally: finally:
scu.GSL.release() scu.GSL.release()
self.store(archive_id, "_description.txt", description.encode("utf-8")) self.store(archive_id, "_description.txt", description)
return archive_id return archive_id
def store(self, archive_id: str, filename: str, data: bytes): def store(self, archive_id: str, filename: str, data: Union[str, bytes]):
"""Store data in archive, under given filename. """Store data in archive, under given filename.
Filename may be modified (sanitized): return used filename Filename may be modified (sanitized): return used filename
The file is created or replaced. The file is created or replaced.
data may be str or bytes
""" """
if isinstance(data, str):
data = data.encode(scu.SCO_ENCODING)
self.initialize() self.initialize()
filename = scu.sanitize_filename(filename) filename = scu.sanitize_filename(filename)
log("storing %s (%d bytes) in %s" % (filename, len(data), archive_id)) log("storing %s (%d bytes) in %s" % (filename, len(data), archive_id))
@ -350,13 +354,11 @@ def do_formsemestre_archive(
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
) )
data = data.encode(scu.SCO_ENCODING)
PVArchive.store(archive_id, "Tableau_moyennes.html", data) PVArchive.store(archive_id, "Tableau_moyennes.html", data)
# Bulletins en JSON # Bulletins en JSON
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True) data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder) data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
data_js = data_js.encode(scu.SCO_ENCODING)
if data: if data:
PVArchive.store(archive_id, "Bulletins.json", data_js) PVArchive.store(archive_id, "Bulletins.json", data_js)
# Decisions de jury, en XLS # Decisions de jury, en XLS

View File

@ -58,7 +58,6 @@ from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_photos
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_pvjury from app.scodoc import sco_pvjury
from app.scodoc import sco_users from app.scodoc import sco_users
@ -66,15 +65,6 @@ import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType, fmt_note from app.scodoc.sco_utils import ModuleType, fmt_note
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
# ----- CLASSES DE BULLETINS DE NOTES
from app.scodoc import sco_bulletins_standard
from app.scodoc import sco_bulletins_legacy
# import sco_bulletins_example # format exemple (à désactiver en production)
# ... ajouter ici vos modules ...
from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun
def get_formsemestre_bulletin_etud_json( def get_formsemestre_bulletin_etud_json(
formsemestre: FormSemestre, formsemestre: FormSemestre,

View File

@ -92,7 +92,6 @@ def formsemestre_bulletinetud_published_dict(
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
d = {"type": "classic", "version": "0"} d = {"type": "classic", "version": "0"}
if (not sem["bul_hide_xml"]) or force_publishing: if (not sem["bul_hide_xml"]) or force_publishing:
published = True published = True
else: else:
@ -134,6 +133,7 @@ def formsemestre_bulletinetud_published_dict(
) )
d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients
# Disponible pour publication ? # Disponible pour publication ?
d["publie"] = published
if not published: if not published:
return d # stop ! return d # stop !
@ -364,8 +364,35 @@ def formsemestre_bulletinetud_published_dict(
return d return d
def dict_decision_jury(etudid, formsemestre_id, with_decisions=False): def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict:
"dict avec decision pour bulletins json" """dict avec decision pour bulletins json
- decision : décision semestre
- decision_ue : list des décisions UE
- situation
with_decision donne les décision même si bul_show_decision est faux.
Exemple:
{
'autorisation_inscription': [{'semestre_id': 4}],
'decision': {'code': 'ADM',
'compense_formsemestre_id': None,
'date': '2022-01-21',
'etat': 'I'},
'decision_ue': [
{
'acronyme': 'UE31',
'code': 'ADM',
'ects': 16.0,
'numero': 23,
'titre': 'Approfondissement métiers',
'ue_id': 1787
},
...
],
'situation': 'Inscrit le 25/06/2021. Décision jury: Validé. UE acquises: '
'UE31, UE32. Diplôme obtenu.'}
"""
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
d = {} d = {}

View File

@ -441,13 +441,13 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
t = { t = {
"titre": ue["acronyme"] + " " + (ue["titre"] or ""), "titre": ue["acronyme"] + " " + (ue["titre"] or ""),
"_titre_html": plusminus "_titre_html": plusminus
+ ue["acronyme"] + (ue["acronyme"] or "")
+ " " + " "
+ ue["titre"] + (ue["titre"] or "")
+ ' <span class="bul_ue_descr">' + ' <span class="bul_ue_descr">'
+ ue["ue_descr_txt"] + (ue["ue_descr_txt"] or "")
+ "</span>", + "</span>",
"_titre_help": ue["ue_descr_txt"], "_titre_help": ue["ue_descr_txt"] or "",
"_titre_colspan": 2, "_titre_colspan": 2,
"module": ue_descr, "module": ue_descr,
"note": ue["moy_ue_txt"], "note": ue["moy_ue_txt"],

View File

@ -189,7 +189,7 @@ CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
CODES_SEM_REO = {NAR: 1} # reorientation CODES_SEM_REO = {NAR: 1} # reorientation
CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée
CODES_RCUE_VALIDES = CODES_UE_VALIDES # Niveau RCUE validé
# Pour le BUT: # Pour le BUT:
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL} CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
CODES_RCUE = {ADM, AJ, CMP} CODES_RCUE = {ADM, AJ, CMP}
@ -201,6 +201,7 @@ BUT_CODES_PASSAGE = {
ADJ, ADJ,
PASD, PASD,
PAS1NCI, PAS1NCI,
ATJ,
} }

View File

@ -683,7 +683,7 @@ def module_edit(
] ]
# Choix des Apprentissages Critiques # Choix des Apprentissages Critiques
if ue is not None: if ue is not None:
annee = f"BUT{orig_semestre_idx//2 + 1}" annee = f"BUT{(orig_semestre_idx+1)//2}"
app_critiques = ApcAppCritique.app_critiques_ref_comp(ref_comp, annee) app_critiques = ApcAppCritique.app_critiques_ref_comp(ref_comp, annee)
descr += ( descr += (
[ [

View File

@ -52,7 +52,6 @@ from app.scodoc.sco_exceptions import (
) )
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_apc from app.scodoc import sco_edit_apc
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
@ -188,7 +187,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
"DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
{"ue_id": ue.id}, {"ue_id": ue.id},
) )
# 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 # delete all matiere in this UE
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id}) mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
for mat in mats: for mat in mats:
@ -448,7 +451,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
<ul>""" <ul>"""
for m in ue.modules: for m in ue.modules:
modules_div += f"""<li><a class="stdlink" href="{url_for( modules_div += f"""<li><a class="stdlink" href="{url_for(
"notes.module_edit",scodoc_dept=g.scodoc_dept, module_id=m.id)}">{m.code} {m.titre}</a></li>""" "notes.module_edit",scodoc_dept=g.scodoc_dept, module_id=m.id)}">{m.code} {m.titre or "sans titre"}</a></li>"""
modules_div += """</ul></div>""" modules_div += """</ul></div>"""
else: else:
modules_div = "" modules_div = ""

View File

@ -59,7 +59,7 @@ class COLORS(Enum):
LIGHT_YELLOW = "FFFFFF99" LIGHT_YELLOW = "FFFFFF99"
# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attributdans la liste suivante: # Un style est enregistré comme un dictionnaire qui précise la valeur d'un attribut dans la liste suivante:
# font, border, number_format, fill,... # font, border, number_format, fill,...
# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles) # (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles)
@ -288,7 +288,7 @@ class ScoExcelSheet:
value -- contenu de la cellule (texte, numérique, booléen ou date) value -- contenu de la cellule (texte, numérique, booléen ou date)
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
""" """
# adapatation des valeurs si nécessaire # adaptation des valeurs si nécessaire
if value is None: if value is None:
value = "" value = ""
elif value is True: elif value is True:

View File

@ -1206,7 +1206,7 @@ def formsemestre_tableau_modules(
) )
H.append( H.append(
'<td class="scotext"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="formsemestre_status_link">%s</a></td>' '<td class="scotext"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="formsemestre_status_link">%s</a></td>'
% (modimpl["moduleimpl_id"], mod_descr, mod.abbrev or mod.titre) % (modimpl["moduleimpl_id"], mod_descr, mod.abbrev or mod.titre or "")
) )
H.append('<td class="formsemestre_status_inscrits">%s</td>' % len(mod_inscrits)) H.append('<td class="formsemestre_status_inscrits">%s</td>' % len(mod_inscrits))
H.append( H.append(

View File

@ -35,13 +35,17 @@ from app.models.etudiants import Identite
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import db, log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
from app.models.notes import etud_has_notes_attente from app.models.notes import etud_has_notes_attente
from app.models.validations import (
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
)
from app.models.but_validations import ApcValidationRCUE, ApcValidationAnnee
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc.sco_codes_parcours import * from app.scodoc.sco_codes_parcours import *
@ -111,7 +115,7 @@ def formsemestre_validation_etud_form(
url_tableau = url_for( url_tableau = url_for(
"notes.formsemestre_recapcomplet", "notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
modejury=1, mode_jury=1,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
selected_etudid=etudid, # va a la bonne ligne selected_etudid=etudid, # va a la bonne ligne
) )
@ -596,10 +600,12 @@ def formsemestre_recap_parcours_table(
title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a></td> title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a></td>
""" """
) )
if decision_sem: if nt.is_apc:
H.append('<td class="rcp_but">BUT</td>')
elif decision_sem:
H.append('<td class="rcp_dec">%s</td>' % decision_sem["code"]) H.append('<td class="rcp_dec">%s</td>' % decision_sem["code"])
else: else:
H.append('<td colspan="%d"><em>en cours</em></td>') H.append("<td><em>en cours</em></td>")
H.append('<td class="rcp_nonass">%s</td>' % ass) # abs H.append('<td class="rcp_nonass">%s</td>' % ass) # abs
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé) # acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
ues = nt.get_ues_stat_dict(filter_sport=True) ues = nt.get_ues_stat_dict(filter_sport=True)
@ -979,7 +985,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
H.append("</ul>") H.append("</ul>")
H.append( H.append(
f"""<a href="{url_for('notes.formsemestre_recapcomplet', f"""<a href="{url_for('notes.formsemestre_recapcomplet',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, modejury=1) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">continuer</a>""" }">continuer</a>"""
) )
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
@ -987,28 +993,32 @@ def do_formsemestre_validation_auto(formsemestre_id):
def formsemestre_validation_suppress_etud(formsemestre_id, etudid): def formsemestre_validation_suppress_etud(formsemestre_id, etudid):
"""Suppression des decisions de jury pour un etudiant.""" """Suppression des décisions de jury pour un étudiant/formsemestre.
log("formsemestre_validation_suppress_etud( %s, %s)" % (formsemestre_id, etudid)) Efface toutes les décisions enregistrées concernant ce formsemestre et cet étudiant:
cnx = ndb.GetDBConnexion() code semestre, UEs, autorisations d'inscription
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) """
args = {"formsemestre_id": formsemestre_id, "etudid": etudid} log(f"formsemestre_validation_suppress_etud( {formsemestre_id}, {etudid})")
try:
# -- Validation du semestre et des UEs # Validations jury classiques (semestres, UEs, autorisations)
cursor.execute( for v in ScolarFormSemestreValidation.query.filter_by(
"""delete from scolar_formsemestre_validation etudid=etudid, formsemestre_id=formsemestre_id
where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s""", ):
args, db.session.delete(v)
) for v in ScolarAutorisationInscription.query.filter_by(
# -- Autorisations d'inscription etudid=etudid, origin_formsemestre_id=formsemestre_id
cursor.execute( ):
"""delete from scolar_autorisation_inscription db.session.delete(v)
where etudid = %(etudid)s and origin_formsemestre_id=%(formsemestre_id)s""", # Validations jury spécifiques BUT
args, for v in ApcValidationRCUE.query.filter_by(
) etudid=etudid, formsemestre_id=formsemestre_id
cnx.commit() ):
except: db.session.delete(v)
cnx.rollback() for v in ApcValidationAnnee.query.filter_by(
raise etudid=etudid, formsemestre_id=formsemestre_id
):
db.session.delete(v)
db.session.commit()
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
_invalidate_etud_formation_caches( _invalidate_etud_formation_caches(

View File

@ -150,22 +150,22 @@ def import_users(users, force=""):
* ok: import ok or aborted * ok: import ok or aborted
* messages: the list of messages * messages: the list of messages
* the # of users created * the # of users created
"""
""" Implémentation: Implémentation:
Pour chaque utilisateur à créer: Pour chaque utilisateur à créer:
* vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé plusieurs fois) * vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé plusieurs fois)
* générer mot de passe aléatoire * générer mot de passe aléatoire
* créer utilisateur et mettre le mot de passe * créer utilisateur et mettre le mot de passe
* envoyer mot de passe par mail * envoyer mot de passe par mail
Les utilisateurs à créer sont stockés dans un dictionnaire. Les utilisateurs à créer sont stockés dans un dictionnaire.
L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée
""" """
created = {} # uid créés
if len(users) == 0: if len(users) == 0:
import_ok = False import_ok = False
msg_list = ["Feuille vide ou illisible"] msg_list = ["Feuille vide ou illisible"]
else: else:
created = {} # liste de uid créés
msg_list = [] msg_list = []
line = 1 # start from excel line #2 line = 1 # start from excel line #2
import_ok = True import_ok = True
@ -217,7 +217,7 @@ def import_users(users, force=""):
else: else:
import_ok = False import_ok = False
except ScoValueError as value_error: except ScoValueError as value_error:
log("import_users: exception: abort create %s" % str(created.keys())) log(f"import_users: exception: abort create {str(created.keys())}")
raise ScoValueError(msg) from value_error raise ScoValueError(msg) from value_error
if import_ok: if import_ok:
for u in created.values(): for u in created.values():
@ -228,7 +228,7 @@ def import_users(users, force=""):
db.session.commit() db.session.commit()
mail_password(u) mail_password(u)
else: else:
created = [] # reset # of created users to 0 created = {} # reset # of created users to 0
return import_ok, msg_list, len(created) return import_ok, msg_list, len(created)

View File

@ -121,7 +121,8 @@ def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
:return: le résultat de la recherche ou None si aucune image trouvée :return: le résultat de la recherche ou None si aucune image trouvée
""" """
allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES) allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
filename_parser = re.compile(f"{prefix}([^.]*).({allowed_ext})") # parse filename 'logo_<logoname>.<ext> . be carefull: logoname may include '.'
filename_parser = re.compile(f"{prefix}(([^.]*.)+)({allowed_ext})")
logos = {} logos = {}
path_dir = Path(scu.SCODOC_LOGOS_DIR) path_dir = Path(scu.SCODOC_LOGOS_DIR)
if dept_id: if dept_id:
@ -135,7 +136,7 @@ def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
if os.access(path_dir.joinpath(entry).absolute(), os.R_OK): if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
result = filename_parser.match(entry.name) result = filename_parser.match(entry.name)
if result: if result:
logoname = result.group(1) logoname = result.group(1)[:-1] # retreive logoname from filename (less final dot)
logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select() logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
return logos if len(logos.keys()) > 0 else None return logos if len(logos.keys()) > 0 else None
@ -191,6 +192,9 @@ class Logo:
) )
self.mm = "Not initialized: call the select or create function before access" self.mm = "Not initialized: call the select or create function before access"
def __repr__(self) -> str:
return f"Logo(logoname='{self.logoname}', filename='{self.filename}')"
def _set_format(self, fmt): def _set_format(self, fmt):
self.suffix = fmt self.suffix = fmt
self.filepath = self.basepath + "." + fmt self.filepath = self.basepath + "." + fmt

View File

@ -36,6 +36,7 @@ from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
from app.but import jury_but_view
from app.models.etudiants import make_etud_args from app.models.etudiants import make_etud_args
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
@ -445,6 +446,10 @@ def ficheEtud(etudid=None):
else: else:
info["groupes_row"] = "" info["groupes_row"] = ""
info["menus_etud"] = menus_etud(etudid) info["menus_etud"] = menus_etud(etudid)
# raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche...
info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid)
tmpl = """<div class="menus_etud">%(menus_etud)s</div> tmpl = """<div class="menus_etud">%(menus_etud)s</div>
<div class="ficheEtud" id="ficheEtud"><table> <div class="ficheEtud" id="ficheEtud"><table>
<tr><td> <tr><td>
@ -477,6 +482,8 @@ def ficheEtud(etudid=None):
%(inscriptions_mkup)s %(inscriptions_mkup)s
%(but_infos_mkup)s
<div class="ficheadmission"> <div class="ficheadmission">
%(adm_data)s %(adm_data)s
@ -513,7 +520,7 @@ def ficheEtud(etudid=None):
""" """
header = html_sco_header.sco_header( header = html_sco_header.sco_header(
page_title="Fiche étudiant %(prenom)s %(nom)s" % info, page_title="Fiche étudiant %(prenom)s %(nom)s" % info,
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"], cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/jury_but.css"],
javascripts=[ javascripts=[
"libjs/jinplace-1.2.1.min.js", "libjs/jinplace-1.2.1.min.js",
"js/ue_list.js", "js/ue_list.js",

View File

@ -109,10 +109,14 @@ class DecisionSem(object):
# log('%s: %s %s %s %s %s' % (self.codechoice,code_etat,new_code_prev,formsemestre_id_utilise_pour_compenser,devenir,assiduite) ) # log('%s: %s %s %s %s %s' % (self.codechoice,code_etat,new_code_prev,formsemestre_id_utilise_pour_compenser,devenir,assiduite) )
def SituationEtudParcours(etud, formsemestre_id): def SituationEtudParcours(etud: dict, formsemestre_id: int):
"""renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)""" """renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# if formsemestre.formation.is_apc():
# return SituationEtudParcoursBUT(etud, formsemestre_id, nt)
parcours = nt.parcours parcours = nt.parcours
# #
if parcours.ECTS_ONLY: if parcours.ECTS_ONLY:
@ -121,10 +125,10 @@ def SituationEtudParcours(etud, formsemestre_id):
return SituationEtudParcoursGeneric(etud, formsemestre_id, nt) return SituationEtudParcoursGeneric(etud, formsemestre_id, nt)
class SituationEtudParcoursGeneric(object): class SituationEtudParcoursGeneric:
"Semestre dans un parcours" "Semestre dans un parcours"
def __init__(self, etud, formsemestre_id, nt): def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat):
""" """
etud: dict filled by fill_etuds_info() etud: dict filled by fill_etuds_info()
""" """
@ -132,7 +136,7 @@ class SituationEtudParcoursGeneric(object):
self.etudid = etud["etudid"] self.etudid = etud["etudid"]
self.formsemestre_id = formsemestre_id self.formsemestre_id = formsemestre_id
self.sem = sco_formsemestre.get_formsemestre(formsemestre_id) self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
self.nt = nt self.nt: NotesTableCompat = nt
self.formation = self.nt.formsemestre.formation self.formation = self.nt.formsemestre.formation
self.parcours = self.nt.parcours self.parcours = self.nt.parcours
# Ce semestre est-il le dernier de la formation ? (e.g. semestre 4 du DUT) # Ce semestre est-il le dernier de la formation ? (e.g. semestre 4 du DUT)

View File

@ -52,7 +52,8 @@ from reportlab.platypus import Paragraph
from reportlab.lib import styles from reportlab.lib import styles
import flask import flask
from flask import url_for, g, redirect, request from flask import flash, redirect, url_for
from flask import g, request
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
@ -274,7 +275,10 @@ def dict_pvjury(
_codes.add(ue["ue_code"]) _codes.add(ue["ue_code"])
d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq]) d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq])
d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"]) if nt.is_apc:
d["decision_sem_descr"] = "" # pas de validation de semestre en BUT
else:
d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"])
d["autorisations"] = sco_parcours_dut.formsemestre_get_autorisation_inscription( d["autorisations"] = sco_parcours_dut.formsemestre_get_autorisation_inscription(
etudid, formsemestre_id etudid, formsemestre_id
@ -501,7 +505,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
from app.but import jury_but_recap from app.but import jury_but_recap
return jury_but_recap.formsemestre_saisie_jury_but( return jury_but_recap.formsemestre_saisie_jury_but(
formsemestre, readonly=True, mode="recap" formsemestre, read_only=True, mode="recap"
) )
# /XXX # /XXX
footer = html_sco_header.sco_footer() footer = html_sco_header.sco_footer()
@ -795,7 +799,7 @@ def descrform_pvjury(sem):
def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]): def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
"Lettres avis jury en PDF" "Lettres avis jury en PDF"
sem = sco_formsemestre.get_formsemestre(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not group_ids: if not group_ids:
# tous les inscrits du semestre # tous les inscrits du semestre
group_ids = [sco_groups.get_default_group(formsemestre_id)] group_ids = [sco_groups.get_default_group(formsemestre_id)]
@ -811,10 +815,15 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
cssstyles=sco_groups_view.CSSSTYLES, cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True, init_qtip=True,
), ),
"""<p class="help">Utiliser cette page pour éditer des versions provisoires des PV. f"""<p class="help">Utiliser cette page pour éditer des versions provisoires des PV.
<span class="fontred">Il est recommandé d'archiver les versions définitives: <a href="formsemestre_archive?formsemestre_id=%s">voir cette page</a></span></p> <span class="fontred">Il est recommandé d'archiver les versions définitives: <a
""" href="{url_for(
% formsemestre_id, "notes.formsemestre_archive",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)}"
>voir cette page</a></span></p>
""",
] ]
F = html_sco_header.sco_footer() F = html_sco_header.sco_footer()
descr = descrform_lettres_individuelles() descr = descrform_lettres_individuelles()
@ -839,7 +848,11 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
return "\n".join(H) + "\n" + tf[1] + F return "\n".join(H) + "\n" + tf[1] + F
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect( return flask.redirect(
"formsemestre_pvjury?formsemestre_id=%s" % (formsemestre_id) url_for(
"notes.formsemestre_pvjury",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )
else: else:
# submit # submit
@ -857,15 +870,17 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
finally: finally:
PDFLOCK.release() PDFLOCK.release()
if not pdfdoc: if not pdfdoc:
flash("Aucun étudiant n'a de décision de jury !")
return flask.redirect( return flask.redirect(
"formsemestre_status?formsemestre_id={}&head_message=Aucun%20%C3%A9tudiant%20n%27a%20de%20d%C3%A9cision%20de%20jury".format( url_for(
formsemestre_id "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
) )
) )
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
dt = time.strftime("%Y-%m-%d")
groups_filename = "-" + groups_infos.groups_filename groups_filename = "-" + groups_infos.groups_filename
filename = "lettres-%s%s-%s.pdf" % (sem["titre_num"], groups_filename, dt) filename = f"""lettres-{formsemestre.titre_num()}{groups_filename}-{time.strftime("%Y-%m-%d")}.pdf"""
return scu.sendPDFFile(pdfdoc, filename) return scu.sendPDFFile(pdfdoc, filename)

View File

@ -45,13 +45,14 @@ from flask import g
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_etud
import sco_version
from app.scodoc.sco_logos import find_logo from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_parcours_dut import SituationEtudParcours
from app.scodoc.sco_pdf import SU from app.scodoc.sco_pdf import SU
import sco_version
LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER
LOGO_FOOTER_HEIGHT = scu.CONFIG.LOGO_FOOTER_HEIGHT * mm LOGO_FOOTER_HEIGHT = scu.CONFIG.LOGO_FOOTER_HEIGHT * mm
@ -62,7 +63,7 @@ LOGO_HEADER_HEIGHT = scu.CONFIG.LOGO_HEADER_HEIGHT * mm
LOGO_HEADER_WIDTH = LOGO_HEADER_HEIGHT * scu.CONFIG.LOGO_HEADER_ASPECT LOGO_HEADER_WIDTH = LOGO_HEADER_HEIGHT * scu.CONFIG.LOGO_HEADER_ASPECT
def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True): def page_footer(canvas, doc, logo, preferences, with_page_numbers=True):
"Add footer on page" "Add footer on page"
width = doc.pagesize[0] # - doc.pageTemplate.left_p - doc.pageTemplate.right_p width = doc.pagesize[0] # - doc.pageTemplate.left_p - doc.pageTemplate.right_p
foot = Frame( foot = Frame(
@ -78,24 +79,24 @@ def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True):
showBoundary=0, showBoundary=0,
) )
LeftFootStyle = reportlab.lib.styles.ParagraphStyle({}) left_foot_style = reportlab.lib.styles.ParagraphStyle({})
LeftFootStyle.fontName = preferences["SCOLAR_FONT"] left_foot_style.fontName = preferences["SCOLAR_FONT"]
LeftFootStyle.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"] left_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
LeftFootStyle.leftIndent = 0 left_foot_style.leftIndent = 0
LeftFootStyle.firstLineIndent = 0 left_foot_style.firstLineIndent = 0
LeftFootStyle.alignment = TA_RIGHT left_foot_style.alignment = TA_RIGHT
RightFootStyle = reportlab.lib.styles.ParagraphStyle({}) right_foot_style = reportlab.lib.styles.ParagraphStyle({})
RightFootStyle.fontName = preferences["SCOLAR_FONT"] right_foot_style.fontName = preferences["SCOLAR_FONT"]
RightFootStyle.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"] right_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
RightFootStyle.alignment = TA_RIGHT right_foot_style.alignment = TA_RIGHT
p = sco_pdf.makeParas( p = sco_pdf.makeParas(
"""<para>%s</para><para>%s</para>""" f"""<para>{preferences["INSTITUTION_NAME"]}</para><para>{
% (preferences["INSTITUTION_NAME"], preferences["INSTITUTION_ADDRESS"]), preferences["INSTITUTION_ADDRESS"]}</para>""",
LeftFootStyle, left_foot_style,
) )
np = Paragraph('<para fontSize="14">%d</para>' % doc.page, RightFootStyle) np = Paragraph(f'<para fontSize="14">{doc.page}</para>', right_foot_style)
tabstyle = TableStyle( tabstyle = TableStyle(
[ [
("LEFTPADDING", (0, 0), (-1, -1), 0), ("LEFTPADDING", (0, 0), (-1, -1), 0),
@ -123,7 +124,7 @@ def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True):
canvas.restoreState() canvas.restoreState()
def pageHeader(canvas, doc, logo, preferences, only_on_first_page=False): def page_header(canvas, doc, logo, preferences, only_on_first_page=False):
if only_on_first_page and int(doc.page) > 1: if only_on_first_page and int(doc.page) > 1:
return return
height = doc.pagesize[1] height = doc.pagesize[1]
@ -260,7 +261,7 @@ class CourrierIndividuelTemplate(PageTemplate):
# ---- Header/Footer # ---- Header/Footer
if self.with_header: if self.with_header:
pageHeader( page_header(
canvas, canvas,
doc, doc,
self.logo_header, self.logo_header,
@ -268,7 +269,7 @@ class CourrierIndividuelTemplate(PageTemplate):
self.header_only_on_first_page, self.header_only_on_first_page,
) )
if self.with_footer: if self.with_footer:
pageFooter( page_footer(
canvas, canvas,
doc, doc,
self.logo_footer, self.logo_footer,
@ -427,7 +428,7 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
""" """
# #
formsemestre_id = sem["formsemestre_id"] formsemestre_id = sem["formsemestre_id"]
Se = decision["Se"] Se: SituationEtudParcours = decision["Se"]
t, s = _descr_jury(sem, Se.parcours_validated() or not Se.semestre_non_terminal) t, s = _descr_jury(sem, Se.parcours_validated() or not Se.semestre_non_terminal)
objects = [] objects = []
style = reportlab.lib.styles.ParagraphStyle({}) style = reportlab.lib.styles.ParagraphStyle({})

View File

@ -57,7 +57,7 @@ from app.scodoc import sco_preferences
def formsemestre_recapcomplet( def formsemestre_recapcomplet(
formsemestre_id=None, formsemestre_id=None,
modejury=False, mode_jury=False,
tabformat="html", tabformat="html",
sortcol=None, sortcol=None,
xml_with_decisions=False, xml_with_decisions=False,
@ -78,7 +78,7 @@ def formsemestre_recapcomplet(
xml, json : concaténation de tous les bulletins, au format demandé xml, json : concaténation de tous les bulletins, au format demandé
pdf : NON SUPPORTE (car tableau trop grand pour générer un pdf utilisable) pdf : NON SUPPORTE (car tableau trop grand pour générer un pdf utilisable)
modejury: cache modules, affiche lien saisie decision jury mode_jury: cache modules, affiche lien saisie decision jury
xml_with_decisions: publie décisions de jury dans xml et json xml_with_decisions: publie décisions de jury dans xml et json
force_publishing: publie les xml et json même si bulletins non publiés force_publishing: publie les xml et json même si bulletins non publiés
selected_etudid: etudid sélectionné (pour scroller au bon endroit) selected_etudid: etudid sélectionné (pour scroller au bon endroit)
@ -91,14 +91,14 @@ def formsemestre_recapcomplet(
if tabformat not in supported_formats: if tabformat not in supported_formats:
raise ScoValueError(f"Format non supporté: {tabformat}") raise ScoValueError(f"Format non supporté: {tabformat}")
is_file = tabformat in file_formats is_file = tabformat in file_formats
modejury = int(modejury) mode_jury = int(mode_jury)
xml_with_decisions = int(xml_with_decisions) xml_with_decisions = int(xml_with_decisions)
force_publishing = int(force_publishing) force_publishing = int(force_publishing)
data = _do_formsemestre_recapcomplet( data = _do_formsemestre_recapcomplet(
formsemestre_id, formsemestre_id,
format=tabformat, format=tabformat,
modejury=modejury, mode_jury=mode_jury,
sortcol=sortcol, sortcol=sortcol,
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing, force_publishing=force_publishing,
@ -123,9 +123,9 @@ def formsemestre_recapcomplet(
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input> <input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
""" """
) )
if modejury: if mode_jury:
H.append( H.append(
f'<input type="hidden" name="modejury" value="{modejury}"></input>' f'<input type="hidden" name="mode_jury" value="{mode_jury}"></input>'
) )
H.append( H.append(
'<select name="tabformat" onchange="document.f.submit()" class="noprint">' '<select name="tabformat" onchange="document.f.submit()" class="noprint">'
@ -163,7 +163,7 @@ def formsemestre_recapcomplet(
) )
if sco_permissions_check.can_validate_sem(formsemestre_id): if sco_permissions_check.can_validate_sem(formsemestre_id):
H.append("<p>") H.append("<p>")
if modejury: if mode_jury:
H.append( H.append(
f"""<a class="stdlink" href="{url_for('notes.formsemestre_validation_auto', f"""<a class="stdlink" href="{url_for('notes.formsemestre_validation_auto',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
@ -172,7 +172,7 @@ def formsemestre_recapcomplet(
else: else:
H.append( H.append(
f"""<a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet', f"""<a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, modejury=1) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">Saisie des décisions du jury</a>""" }">Saisie des décisions du jury</a>"""
) )
H.append("</p>") H.append("</p>")
@ -196,7 +196,7 @@ def _do_formsemestre_recapcomplet(
formsemestre_id=None, formsemestre_id=None,
format="html", # html, xml, xls, xlsall, json format="html", # html, xml, xls, xlsall, json
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML) xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
modejury=False, # saisie décisions jury mode_jury=False, # saisie décisions jury
sortcol=None, # indice colonne a trier dans table T sortcol=None, # indice colonne a trier dans table T
xml_with_decisions=False, xml_with_decisions=False,
force_publishing=True, force_publishing=True,
@ -215,7 +215,7 @@ def _do_formsemestre_recapcomplet(
formsemestre, formsemestre,
res, res,
include_evaluations=(format == "evals"), include_evaluations=(format == "evals"),
modejury=modejury, mode_jury=mode_jury,
filename=filename, filename=filename,
selected_etudid=selected_etudid, selected_etudid=selected_etudid,
) )
@ -368,34 +368,34 @@ def gen_formsemestre_recapcomplet_html(
formsemestre: FormSemestre, formsemestre: FormSemestre,
res: NotesTableCompat, res: NotesTableCompat,
include_evaluations=False, include_evaluations=False,
modejury=False, mode_jury=False,
filename="", filename="",
selected_etudid=None, selected_etudid=None,
): ):
"""Construit table recap pour le BUT """Construit table recap pour le BUT
Cache le résultat pour le semestre (sauf en mode jury). Cache le résultat pour le semestre (sauf en mode jury).
Si modejury, cache colonnes modules et affiche un lien vers la saisie de la décision de jury Si mode_jury, cache colonnes modules et affiche un lien vers la saisie de la décision de jury
Return: data, filename Return: data, filename
data est une chaine, le <div>...</div> incluant le tableau. data est une chaine, le <div>...</div> incluant le tableau.
""" """
table_html = None table_html = None
if not (modejury or selected_etudid): if not (mode_jury or selected_etudid):
if include_evaluations: if include_evaluations:
table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id) table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id)
else: else:
table_html = sco_cache.TableRecapCache.get(formsemestre.id) table_html = sco_cache.TableRecapCache.get(formsemestre.id)
if modejury or (table_html is None): if mode_jury or (table_html is None):
table_html = _gen_formsemestre_recapcomplet_html( table_html = _gen_formsemestre_recapcomplet_html(
formsemestre, formsemestre,
res, res,
include_evaluations, include_evaluations,
modejury, mode_jury,
filename, filename,
selected_etudid=selected_etudid, selected_etudid=selected_etudid,
) )
if not modejury: if not mode_jury:
if include_evaluations: if include_evaluations:
sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html) sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
else: else:
@ -408,13 +408,15 @@ def _gen_formsemestre_recapcomplet_html(
formsemestre: FormSemestre, formsemestre: FormSemestre,
res: NotesTableCompat, res: NotesTableCompat,
include_evaluations=False, include_evaluations=False,
modejury=False, mode_jury=False,
filename: str = "", filename: str = "",
selected_etudid=None, selected_etudid=None,
) -> str: ) -> str:
"""Génère le html""" """Génère le html"""
rows, footer_rows, titles, column_ids = res.get_table_recap( rows, footer_rows, titles, column_ids = res.get_table_recap(
convert_values=True, include_evaluations=include_evaluations, modejury=modejury convert_values=True,
include_evaluations=include_evaluations,
mode_jury=mode_jury,
) )
if not rows: if not rows:
return ( return (
@ -423,7 +425,7 @@ def _gen_formsemestre_recapcomplet_html(
H = [ H = [
f"""<div class="table_recap"><table class="table_recap { f"""<div class="table_recap"><table class="table_recap {
'apc' if formsemestre.formation.is_apc() else 'classic' 'apc' if formsemestre.formation.is_apc() else 'classic'
} {'jury' if modejury else ''}" } {'jury' if mode_jury else ''}"
data-filename="{filename}">""" data-filename="{filename}">"""
] ]
# header # header

View File

@ -588,7 +588,7 @@ def purge_chars(s, allowed_chars=""):
return s.translate(PurgeChars(allowed_chars=allowed_chars)) return s.translate(PurgeChars(allowed_chars=allowed_chars))
def sanitize_string(s): def sanitize_string(s, remove_spaces=True):
"""s is an ordinary string, encoding given by SCO_ENCODING" """s is an ordinary string, encoding given by SCO_ENCODING"
suppress accents and chars interpreted in XML suppress accents and chars interpreted in XML
Irreversible (not a quote) Irreversible (not a quote)
@ -596,8 +596,10 @@ def sanitize_string(s):
For ids and some filenames For ids and some filenames
""" """
# Table suppressing some chars: # Table suppressing some chars:
trans = str.maketrans("", "", "'`\"<>!&\\ ") to_del = "'`\"<>!&\\ " if remove_spaces else "'`\"<>!&"
return suppress_accents(s.translate(trans)).replace(" ", "_").replace("\t", "_") trans = str.maketrans("", "", to_del)
return suppress_accents(s.translate(trans)).replace("\t", "_")
_BAD_FILENAME_CHARS = str.maketrans("", "", ":/\\&[]*?'") _BAD_FILENAME_CHARS = str.maketrans("", "", ":/\\&[]*?'")
@ -968,6 +970,8 @@ ICON_XLS = icontag("xlsicon_img", title="Version tableur")
# HTML emojis # HTML emojis
EMO_WARNING = "&#9888;&#65039;" # warning /!\ EMO_WARNING = "&#9888;&#65039;" # warning /!\
EMO_RED_TRIANGLE_DOWN = "&#128315;" # red triangle pointed down EMO_RED_TRIANGLE_DOWN = "&#128315;" # red triangle pointed down
EMO_PREV_ARROW = "&#10094;"
EMO_NEXT_ARROW = "&#10095;"
def sort_dates(L, reverse=False): def sort_dates(L, reverse=False):
@ -1097,6 +1101,10 @@ def gen_cell(key: str, row: dict, elt="td", with_col_class=False):
if with_col_class: if with_col_class:
klass = key + " " + klass klass = key + " " + klass
attrs = f'class="{klass}"' if klass else "" attrs = f'class="{klass}"' if klass else ""
data = row.get(f"_{key}_data") # dict
if data:
for k in data:
attrs += f' data-{k}="{data[k]}"'
order = row.get(f"_{key}_order") order = row.get(f"_{key}_order")
if order: if order:
attrs += f' data-order="{order}"' attrs += f' data-order="{order}"'

View File

@ -65,6 +65,19 @@
font-weight: bold; font-weight: bold;
} }
.but_navigation {
padding-top: 16px;
margin-left: 50px;
margin-right: 50px;
}
.but_navigation div {
display: inline-block;
margin-left: 50px;
margin-right: 50px;
}
div.but_section_annee { div.but_section_annee {
margin-bottom: 10px; margin-bottom: 10px;
} }
@ -73,9 +86,10 @@ div.but_settings {
margin-top: 16px; margin-top: 16px;
} }
span.but_explanation { .but_explanation {
color: blueviolet; color: blueviolet;
font-style: italic; font-style: italic;
padding-top: 12px;
} }
select:disabled { select:disabled {

View File

@ -189,10 +189,10 @@ section>div:nth-child(1){
font-weight: bold; font-weight: bold;
font-size: 20px; font-size: 20px;
} }
#ects_tot { #ects_tot, .decision, .decision_annee {
margin-left: 8px;
font-weight: bold; font-weight: bold;
font-size: 20px; font-size: 20px;
margin-top: 8px;
} }
.enteteSemestre{ .enteteSemestre{
color: black; color: black;

View File

@ -2980,7 +2980,8 @@ td.rcp_dec {
; ;
} }
td.rcp_nonass { td.rcp_nonass,
td.rcp_but {
color: red; color: red;
} }
@ -3770,6 +3771,7 @@ table.table_recap .rang {
} }
table.table_recap .col_ue, table.table_recap .col_ue,
table.table_recap .col_ue_code,
table.table_recap .col_moy_gen, table.table_recap .col_moy_gen,
table.table_recap .group { table.table_recap .group {
border-left: 1px solid blue; border-left: 1px solid blue;
@ -3783,15 +3785,18 @@ table.table_recap.jury .col_ue {
font-weight: normal; font-weight: normal;
} }
table.table_recap.jury .col_rcue { table.table_recap.jury .col_rcue,
table.table_recap.jury .col_rcue_code {
font-weight: bold; font-weight: bold;
} }
table.table_recap.jury tr.even td.col_rcue { table.table_recap.jury tr.even td.col_rcue,
table.table_recap.jury tr.even td.col_rcue_code {
background-color: #b0d4f8; background-color: #b0d4f8;
} }
table.table_recap.jury tr.odd td.col_rcue { table.table_recap.jury tr.odd td.col_rcue,
table.table_recap.jury tr.odd td.col_rcue_code {
background-color: #abcdef; background-color: #abcdef;
} }

View File

@ -11,4 +11,52 @@ function change_menu_code(elt) {
// TODO: comparer avec valeur enregistrée (à mettre en data-orig ?) // TODO: comparer avec valeur enregistrée (à mettre en data-orig ?)
// et colorer en fonction // et colorer en fonction
elt.parentElement.parentElement.classList.add("modified"); elt.parentElement.parentElement.classList.add("modified");
} }
$(function () {
// Recupère la liste ordonnées des etudids
// pour avoir le "suivant" etr le "précédent"
// (liens de navigation)
const url = new URL(document.URL);
const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid
const etudid = frags[frags.length - 1];
const formsemestre_id = frags[frags.length - 2];
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
const etudids_str = localStorage.getItem(etudids_key);
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
const noms_str = localStorage.getItem(noms_key);
if (etudids_str && noms_str) {
const etudids = JSON.parse(etudids_str);
const noms = JSON.parse(noms_str);
const cur_idx = etudids.indexOf(etudid);
let prev_idx = -1;
let next_idx = -1
if (cur_idx != -1) {
if (cur_idx > 0) {
prev_idx = cur_idx - 1;
}
if (cur_idx < etudids.length - 1) {
next_idx = cur_idx + 1;
}
}
if (prev_idx != -1) {
let elem = document.querySelector("div.prev a");
if (elem) {
elem.href = elem.href.replace("PREV", etudids[prev_idx]);
elem.innerHTML = noms[prev_idx];
}
} else {
document.querySelector("div.prev").innerHTML = "";
}
if (next_idx != -1) {
let elem = document.querySelector("div.next a");
if (elem) {
elem.href = elem.href.replace("NEXT", etudids[next_idx]);
elem.innerHTML = noms[next_idx];
}
} else {
document.querySelector("div.next").innerHTML = "";
}
}
});

View File

@ -83,7 +83,9 @@ class releveBUT extends HTMLElement {
<div> <div>
<div class=infoSemestre></div> <div class=infoSemestre></div>
<div> <div>
<div><span class=decision></span><span class="ects" id="ects_tot"></span></div> <div class=decision_annee></div>
<div class=decision></div>
<div class="ects" id="ects_tot"></div>
<div class=dateInscription>Inscrit le </div> <div class=dateInscription>Inscrit le </div>
<em>Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em> <em>Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em>
</div> </div>
@ -192,6 +194,20 @@ class releveBUT extends HTMLElement {
/* Information sur le semestre */ /* Information sur le semestre */
/*******************************/ /*******************************/
showSemestre(data) { showSemestre(data) {
let correspondanceCodes = {
"ADM": "Admis",
"AJD": "Admis par décision de jury",
"PASD": "Passage de droit : tout n'est pas validé, mais d'après les règles du BUT, vous passez",
"PAS1NCI": "Vous passez par décision de jury mais attention, vous n'avez pas partout le niveau suffisant",
"RED": "Ajourné mais autorisé à redoubler",
"NAR": "Non admis et non autorisé à redoubler : réorientation",
"DEM": "Démission",
"ABAN": "Abandon constaté sans lettre de démission",
"RAT": "En attente d'un rattrapage",
"EXCLU": "Exclusion dans le cadre d'une décision disciplinaire",
"DEF": "Défaillance : non évalué par manque d'assiduité",
"ABL": "Année blanche"
}
this.shadow.querySelector("#identite_etudiant").innerHTML = ` <a href="${data.etudiant.fiche_url}">${data.etudiant.nomprenom}</a> `; this.shadow.querySelector("#identite_etudiant").innerHTML = ` <a href="${data.etudiant.fiche_url}">${data.etudiant.nomprenom}</a> `;
this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription); this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription);
@ -208,9 +224,26 @@ class releveBUT extends HTMLElement {
<div class=abs>Non justifiées</div> <div class=abs>Non justifiées</div>
<div>${data.semestre.absences?.injustifie ?? "-"}</div> <div>${data.semestre.absences?.injustifie ?? "-"}</div>
<div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</div> <div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</div>
</div> </div>`;
<a class=photo href="${data.etudiant.fiche_url}"><img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0"></a> if(data.semestre.decision_rcue?.length){
`; output += `
<div>
<div class=enteteSemestre>RCUE</div><div></div>
${(()=>{
let output = "";
data.semestre.decision_rcue.forEach(competence=>{
output += `<div class=rang>${competence.niveau.competence.titre}</div><div>${competence.code}</div>`;
})
return output;
})()}
</div>
</div>`
}
output += `
<a class=photo href="${data.etudiant.fiche_url}">
<img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0">
</a>`;
/*${data.semestre.groupes.map(groupe => { /*${data.semestre.groupes.map(groupe => {
return ` return `
<div> <div>
@ -224,10 +257,17 @@ class releveBUT extends HTMLElement {
}).join("") }).join("")
}*/ }*/
this.shadow.querySelector(".infoSemestre").innerHTML = output; this.shadow.querySelector(".infoSemestre").innerHTML = output;
if (data.semestre.decision?.code) {
/*if(data.semestre.decision_annee?.code){
this.shadow.querySelector(".decision_annee").innerHTML = "Décision année : " + data.semestre.decision_annee.code + " - " + correspondanceCodes[data.semestre.decision_annee.code];
}*/
this.shadow.querySelector(".decision").innerHTML = data.semestre.situation || "";
/*if (data.semestre.decision?.code) {
this.shadow.querySelector(".decision").innerHTML = "Décision jury: " + (data.semestre.decision?.code || ""); this.shadow.querySelector(".decision").innerHTML = "Décision jury: " + (data.semestre.decision?.code || "");
} }*/
this.shadow.querySelector("#ects_tot").innerHTML = "ECTS&nbsp;:&nbsp;" + (data.semestre.ECTS?.acquis || "-") + "&nbsp;/&nbsp;" + (data.semestre.ECTS?.total || "-"); this.shadow.querySelector("#ects_tot").innerHTML = "ECTS&nbsp;:&nbsp;" + (data.semestre.ECTS?.acquis ?? "-") + "&nbsp;/&nbsp;" + (data.semestre.ECTS?.total ?? "-");
} }
/*******************************/ /*******************************/
@ -254,13 +294,13 @@ class releveBUT extends HTMLElement {
${ue}${(dataUE.titre) ? " - " + dataUE.titre : ""} ${ue}${(dataUE.titre) ? " - " + dataUE.titre : ""}
</h3> </h3>
<div> <div>
<div class=moyenne>Moyenne&nbsp;:&nbsp;${dataUE.moyenne?.value || "-"}</div> <div class=moyenne>Moyenne&nbsp;:&nbsp;${dataUE.moyenne?.value ?? "-"}</div>
<div class=rang>Rang&nbsp;:&nbsp;${dataUE.moyenne?.rang}&nbsp;/&nbsp;${dataUE.moyenne?.total}</div> <div class=rang>Rang&nbsp;:&nbsp;${dataUE.moyenne?.rang}&nbsp;/&nbsp;${dataUE.moyenne?.total}</div>
<div class=info> <div class=info>
Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;- Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;-
Malus&nbsp;:&nbsp;${dataUE.malus || 0} Malus&nbsp;:&nbsp;${dataUE.malus || 0}
<span class=ects>&nbsp;- <span class=ects>&nbsp;-
ECTS&nbsp;:&nbsp;${dataUE.ECTS?.acquis || "-"}&nbsp;/&nbsp;${dataUE.ECTS?.total || "-"} ECTS&nbsp;:&nbsp;${dataUE.ECTS?.acquis ?? "-"}&nbsp;/&nbsp;${dataUE.ECTS?.total ?? "-"}
</span> </span>
</div> </div>
</div>`; </div>`;

View File

@ -11,6 +11,7 @@ function build_table(data) {
let output = ""; let output = "";
let sumsUE = {}; let sumsUE = {};
let sumsRessources = {}; let sumsRessources = {};
let value;
data.forEach((cellule) => { data.forEach((cellule) => {
output += ` output += `
@ -31,13 +32,16 @@ function build_table(data) {
--y:${cellule.y}; --y:${cellule.y};
--nbX:${cellule.nbX || 1}; --nbX:${cellule.nbX || 1};
--nbY: ${cellule.nbY || 1}; --nbY: ${cellule.nbY || 1};
"> ">${cellule.data}</div>`; // ne pas mettre d'espace car c'est utilisé par :not(:empty) après
${cellule.data}
</div>`;
if (cellule.style.includes("champs")) { if (cellule.style.includes("champs")) {
sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + (parseFloat(cellule.data) || 0); if (cellule.editable == true && cellule.data) {
sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + (parseFloat(cellule.data) || 0); value = parseFloat(cellule.data) *100;
} else {
value = 0;
}
sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + value;
sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + value;
} }
}) })
@ -65,7 +69,7 @@ function showSums(sumsRessources, sumsUE) {
--nbX:1; --nbX:1;
--nbY:1; --nbY:1;
"> ">
${value} ${value / 100}
</div>`; </div>`;
}) })
@ -82,7 +86,7 @@ function showSums(sumsRessources, sumsUE) {
--nbX:1; --nbX:1;
--nbY:1; --nbY:1;
"> ">
${value} ${value / 100}
</div>`; </div>`;
}) })
@ -186,16 +190,16 @@ function keyCell(event) {
function processSums() { function processSums() {
let sum = 0; let sum = 0;
document.querySelectorAll(`[data-editable="true"][data-x="${this.dataset.x}"]`).forEach(e => { document.querySelectorAll(`[data-editable="true"][data-x="${this.dataset.x}"]:not(:empty)`).forEach(e => {
sum += parseFloat(e.innerText) || 0; sum += parseFloat(e.innerText) * 100;
}) })
document.querySelector(`.sums[data-x="${this.dataset.x}"][data-y="${lastY}"]`).innerText = sum; document.querySelector(`.sums[data-x="${this.dataset.x}"][data-y="${lastY}"]`).innerText = sum / 100;
sum = 0; sum = 0;
document.querySelectorAll(`[data-editable="true"][data-y="${this.dataset.y}"]`).forEach(e => { document.querySelectorAll(`[data-editable="true"][data-y="${this.dataset.y}"]:not(:empty)`).forEach(e => {
sum += parseFloat(e.innerText) || 0; sum += parseFloat(e.innerText) * 100;
}) })
document.querySelector(`.sums[data-x="${lastX}"][data-y="${this.dataset.y}"]`).innerText = sum; document.querySelector(`.sums[data-x="${lastX}"][data-y="${this.dataset.y}"]`).innerText = sum / 100;
} }
/******************************/ /******************************/

View File

@ -133,7 +133,7 @@ $(function () {
} }
}); });
} }
$('table.table_recap').DataTable( let table = $('table.table_recap').DataTable(
{ {
paging: false, paging: false,
searching: true, searching: true,
@ -146,6 +146,7 @@ $(function () {
orderCellsTop: true, // cellules ligne 1 pour tri orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting aaSorting: [], // Prevent initial sorting
colReorder: true, colReorder: true,
stateSave: true, // enregistre état de la table (tris, ...)
"columnDefs": [ "columnDefs": [
{ {
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
@ -154,7 +155,7 @@ $(function () {
}, },
{ {
// Elimine les 0 à gauche pour les exports excel et les "copy" // Elimine les 0 à gauche pour les exports excel et les "copy"
targets: ["col_mod", "col_moy_gen", "col_ue", "col_res", "col_sae", "evaluation"], targets: ["col_mod", "col_moy_gen", "col_ue", "col_res", "col_sae", "evaluation", "col_rcue"],
render: function (data, type, row) { render: function (data, type, row) {
return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data; return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
} }
@ -192,11 +193,22 @@ $(function () {
if (formsemestre_id) { if (formsemestre_id) {
localStorage.setItem(order_info_key, order_info); localStorage.setItem(order_info_key, order_info);
} }
let etudids = [];
document.querySelectorAll("td.col_rcues_validables").forEach(e => {
etudids.push(e.dataset.etudid);
});
let noms = [];
document.querySelectorAll("td.col_rcues_validables").forEach(e => {
noms.push(e.dataset.nomprenom);
});
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
localStorage.setItem(etudids_key, JSON.stringify(etudids));
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
localStorage.setItem(noms_key, JSON.stringify(noms));
}, },
"order": order_info, "order": order_info,
} }
); );
}); });
$('table.table_recap tbody').on('click', 'tr', function () { $('table.table_recap tbody').on('click', 'tr', function () {
if ($(this).hasClass('selected')) { if ($(this).hasClass('selected')) {
@ -211,8 +223,8 @@ $(function () {
$(function () { $(function () {
let row_selected = document.querySelector("#row_selected"); let row_selected = document.querySelector("#row_selected");
if (row_selected) { if (row_selected) {
row_selected.scrollIntoView(); /*row_selected.scrollIntoView();
window.scrollBy(0, -50); window.scrollBy(0, -50);*/
row_selected.classList.add("selected"); row_selected.classList.add("selected");
} }
}); });

View File

@ -1,9 +1,12 @@
<div class="but_doc_codes"> <div class="but_doc_codes">
<p><em>Ci-dessous la signification de chaque code est expliquée, <p><em>Ci-dessous la signification de chaque code est expliquée,
ainsi que la correspondance avec les codes préconisés par ainsi que la correspondance avec certains codes préconisés par
l'AMUE pour Apogée dans un document informel qui a circulé début l'AMUE et l'ADIUT pour Apogée.
2022 (les éventuelles erreurs n'engagent personne). </em>
</em></p> On distingue les codes ScoDoc (utilisés ci-dessus et dans les différentes
tables générées par ScoDoc) et leur transcription vers Apogée lors des exports
(transcription paramétrable par votre administrateur ScoDoc).
</p>
<div class="but_doc_section">Codes d'année</div> <div class="but_doc_section">Codes d'année</div>
<div class="but_doc"> <div class="but_doc">
<table> <table>
@ -63,6 +66,12 @@
<td class="amue">ABAN</td> <td class="amue">ABAN</td>
<td>ABANdon constaté (sans lettre de démission)</td> <td>ABANdon constaté (sans lettre de démission)</td>
</tr> </tr>
<tr>
<td>ATJ</td>
<td>{{codes["ATJ"]}}</td>
<td class="amue">nd</td>
<td>Non validé pour une autre raison, voir règlement local</td>
</tr>
<tr> <tr>
<td>RAT</td> <td>RAT</td>
<td>{{codes["RAT"]}}</td> <td>{{codes["RAT"]}}</td>
@ -124,6 +133,12 @@
<td class="amue">AJ</td> <td class="amue">AJ</td>
<td>Attente pour problème de moyenne</td> <td>Attente pour problème de moyenne</td>
</tr> </tr>
<tr>
<td>ATJ</td>
<td>{{codes["ATJ"]}}</td>
<td class="amue">nd</td>
<td>Non validé pour une autre raison, voir règlement local</td>
</tr>
<tr> <tr>
<td>RAT</td> <td>RAT</td>
<td>{{codes["RAT"]}}</td> <td>{{codes["RAT"]}}</td>
@ -180,6 +195,12 @@
<td class="amue">AJ</td> <td class="amue">AJ</td>
<td>Attente pour problème de moyenne</td> <td>Attente pour problème de moyenne</td>
</tr> </tr>
<tr>
<td>ATJ</td>
<td>{{codes["ATJ"]}}</td>
<td class="amue">nd</td>
<td>Non validé pour une autre raison, voir règlement local</td>
</tr>
<tr> <tr>
<td>RAT</td> <td>RAT</td>
<td>{{codes["RAT"]}}</td> <td>{{codes["RAT"]}}</td>
@ -212,4 +233,34 @@
</tr> </tr>
</table> </table>
</div> </div>
<div class="but_doc_section">Rappels de l'arrêté BUT (extraits)</div>
<div class="but_doc">
<ul>
<li>Au sein de chaque regroupement cohérent dUE, la compensation est intégrale.
Si une UE na pas été acquise en raison dune moyenne inférieure à 10,
cette UE sera acquise par compensation si et seulement si létudiant
a obtenu la moyenne au regroupement cohérent auquel lUE appartient.</li>
<li>La poursuite d'études dans un semestre pair dune même année est de droit
pour tout étudiant.
La poursuite détudes dans un semestre impair est possible
<em>si et seulement si</em> létudiant a obtenu :
<ul>
<li>la moyenne à plus de la moitié des regroupements cohérents dUE</li>
<li>et une moyenne égale ou supérieure à 8 sur 20 à chaque regroupement cohérent dUE.</li>
</ul>
</li>
<li>La poursuite d'études dans le semestre 5 nécessite de plus la validation de toutes les UE des
semestres 1 et 2 dans les conditions de validation des points 4.3 et 4.4, ou par décision de jury.</li>
</ul>
<b>Textes de référence:</b>
<ul>
<li><a href="https://www.enseignementsup-recherche.gouv.fr/fr/bo/21/Special4/ESRS2114777A.htm">Bulletin
officiel spécial n°4 du 17 juin 2021</a></li>
<li><a
href="https://cache.media.enseignementsup-recherche.gouv.fr//file/SPE4-MESRI-17-6-2021/19/4/SP4_ESR_17_6_2021_1413194.pdf">Version
pdf complète</a></li>
</ul>
</div>
</div> </div>

View File

@ -31,7 +31,6 @@ Module notes: issu de ScoDoc7 / ZNotes.py
Emmanuel Viennet, 2021 Emmanuel Viennet, 2021
""" """
import html
from operator import itemgetter from operator import itemgetter
import time import time
from xml.etree import ElementTree from xml.etree import ElementTree
@ -43,6 +42,8 @@ from flask_login import current_user
from app.but import jury_but, jury_but_validation_auto from app.but import jury_but, jury_but_validation_auto
from app.but.forms import jury_but_forms from app.but.forms import jury_but_forms
from app.but import jury_but_pv
from app.but import jury_but_view
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
@ -56,23 +57,21 @@ from app.models.ues import UniteEns
from app import api from app import api
from app import db from app import db
from app import models from app import models
from app.models import ScolarNews from app.models import ScolarNews, but_validations
from app.auth.models import User from app.auth.models import User
from app.but import apc_edit_ue, bulletin_but, jury_but_recap from app.but import apc_edit_ue, jury_but_recap
from app.decorators import ( from app.decorators import (
scodoc, scodoc,
scodoc7func, scodoc7func,
permission_required, permission_required,
permission_required_compat_scodoc7, permission_required_compat_scodoc7,
admin_required,
login_required,
) )
from app.views import notes_bp as bp from app.views import notes_bp as bp
# --------------- # ---------------
from app.scodoc import sco_utils as scu from app.scodoc import sco_bulletins_json, sco_utils as scu
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm from app import log, send_scodoc_alarm
@ -297,7 +296,7 @@ def formsemestre_bulletinetud(
format = format or "html" format = format or "html"
if not isinstance(formsemestre_id, int): if not isinstance(formsemestre_id, int):
abort(404, description="formsemestre_id must be an integer !") raise ScoInvalidIdType("formsemestre_id must be an integer !")
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if etudid: if etudid:
etud = models.Identite.query.get_or_404(etudid) etud = models.Identite.query.get_or_404(etudid)
@ -2144,6 +2143,16 @@ def formsemestre_validation_etud_form(
): ):
"Formulaire choix jury pour un étudiant" "Formulaire choix jury pour un étudiant"
readonly = not sco_permissions_check.can_validate_sem(formsemestre_id) readonly = not sco_permissions_check.can_validate_sem(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc():
return redirect(
url_for(
"notes.formsemestre_validation_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
)
return sco_formsemestre_validation.formsemestre_validation_etud_form( return sco_formsemestre_validation.formsemestre_validation_etud_form(
formsemestre_id, formsemestre_id,
etudid=etudid, etudid=etudid,
@ -2217,22 +2226,24 @@ def formsemestre_validation_etud_manu(
# --- Jurys BUT # --- Jurys BUT
@bp.route( @bp.route(
"/formsemestre_validation_but/<int:formsemestre_id>/<int:etudid>", "/formsemestre_validation_but/<int:formsemestre_id>/<etudid>",
methods=["GET", "POST"], methods=["GET", "POST"],
) )
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def formsemestre_validation_but(formsemestre_id: int, etudid: int): def formsemestre_validation_but(
formsemestre_id: int,
etudid: int,
):
"Form. saisie décision jury semestre BUT" "Form. saisie décision jury semestre BUT"
if not sco_permissions_check.can_validate_sem(formsemestre_id): # la route ne donne pas le type d'etudid pour pouvoir construire des URLs
return scu.confirm_dialog( # provisoires avec NEXT et PREV
message=f"<p>Opération non autorisée pour {current_user}</h2>", try:
dest_url=url_for( etudid = int(etudid)
"notes.formsemestre_status", except:
scodoc_dept=g.scodoc_dept, abort(404, "invalid etudid")
formsemestre_id=formsemestre_id, read_only = not sco_permissions_check.can_validate_sem(formsemestre_id)
),
)
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Validation BUT", page_title="Validation BUT",
@ -2265,13 +2276,13 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()
) )
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if len(deca.rcues_annee) == 0: if len(deca.rcues_annee) == 0:
raise ScoValueError("année incomplète: pas de jury BUT annuel possible") raise ScoValueError("année incomplète: pas de jury BUT annuel possible")
if request.method == "POST": if request.method == "POST":
deca.record_form(request.form) if not read_only:
flash("codes enregistrés") deca.record_form(request.form)
flash("codes enregistrés")
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.formsemestre_validation_but", "notes.formsemestre_validation_but",
@ -2280,13 +2291,7 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
etudid=etudid, etudid=etudid,
) )
) )
if deca.code_valide:
erase_span = f"""<a href="{
url_for("notes.formsemestre_jury_but_erase",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id,
etudid=etudid)}" class="stdlink">effacer décisions</a>"""
else:
erase_span = ""
warning = "" warning = ""
if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau): if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau):
warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)} warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)}
@ -2296,95 +2301,77 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
H.append( H.append(
f""" f"""
<div> <div>
<div class="titre_parcours">Jury BUT{deca.annee_but} <div class="bull_head">
- Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"} <div>
- {deca.annee_scolaire_str()}</div> <div class="titre_parcours">Jury BUT{deca.annee_but}
<div class="nom_etud">{etud.nomprenom}</div> - Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"}
{warning} - {deca.annee_scolaire_str()}</div>
<div class="nom_etud">{etud.nomprenom}</div>
</div>
<div class="bull_photo"><a href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div>
</div>
{warning}
</div> </div>
<form method="POST"> <form method="POST">
<div class="but_section_annee">
<div>
<b>Décision de jury pour l'année :</b> {
_gen_but_select("code_annee", deca.codes, deca.code_valide,
disabled=True, klass="manual")
}
<span>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
<span>{erase_span}</span>
</div>
<span class="but_explanation">{deca.explanation}</span>
</div>
<div><b>Niveaux de compétences et unités d'enseignement :</b></div>
<div class="but_annee">
<div class="titre"></div>
<div class="titre">S{1}</div>
<div class="titre">S{2}</div>
<div class="titre">RCUE</div>
""" """
) )
for niveau in deca.niveaux_competences: H.append(jury_but_view.show_etud(deca, read_only=read_only))
H.append(
f"""<div class="but_niveau_titre">
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
</div>"""
)
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id)
if dec_rcue is None:
break
# Semestre impair
H.append(
_gen_but_niveau_ue(
dec_rcue.rcue.ue_1,
dec_rcue.rcue.moy_ue_1,
deca.decisions_ues[dec_rcue.rcue.ue_1.id],
)
)
# Semestre pair
H.append(
_gen_but_niveau_ue(
dec_rcue.rcue.ue_2,
dec_rcue.rcue.moy_ue_2,
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
)
)
# RCUE
H.append(
f"""<div class="but_niveau_rcue
{'recorded' if dec_rcue.code_valide is not None else ''}
">
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
<div class="but_code">{
_gen_but_select("code_rcue_"+str(niveau.id),
dec_rcue.codes,
dec_rcue.code_valide,
disabled=True, klass="manual"
)
}</div>
</div>"""
)
H.append("</div>") # but_annee
if read_only:
H.append(
"""<div class="but_explanation">Vous n'avez pas la permission de modifier ces décisions.
Les champs entourés en vert sont enregistrés.</div>"""
)
else:
H.append(
f"""<div class="but_settings">
<input type="checkbox" onchange="enable_manual_codes(this)">
<em>permettre la saisie manuelles des codes d'année et de niveaux.
Dans ce cas, il vous revient de vous assurer de la cohérence entre
vos codes d'UE/RCUE/Année !</em>
</input>
</div>
<div class="but_buttons">
<input type="submit" value="Enregistrer ces décisions">
</div>
"""
)
# --- Navigation
prev = f"""{scu.EMO_PREV_ARROW}&nbsp;<a href="{url_for(
"notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, etudid="PREV"
)}" class="stdlink"">précédent</a>
"""
next = f"""<a href="{url_for(
"notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, etudid="NEXT"
)}" class="stdlink"">suivant</a>&nbsp;{scu.EMO_NEXT_ARROW}
"""
H.append( H.append(
f"""<div class="but_settings"> f"""
<input type="checkbox" onchange="enable_manual_codes(this)"> <div class="but_navigation">
<em>permettre la saisie manuelles des codes d'année et de niveaux. <div class="prev">
Dans ce cas, il vous revient de vous assurer de la cohérence entre {prev}
vos codes d'UE/RCUE/Année !</em>
</input>
</div> </div>
<div class="back_list">
<div class="but_buttons"> <a href="{url_for(
<span><input type="submit" value="Enregistrer ces décisions"></span>
<span><a href="{url_for(
"notes.formsemestre_saisie_jury", scodoc_dept=g.scodoc_dept, "notes.formsemestre_saisie_jury", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, selected_etudid=etud.id formsemestre_id=formsemestre_id, selected_etudid=etud.id
)}">retour à la liste</a></span> )}" class="stdlink">retour à la liste</a>
</div> </div>
""" <div class="next">
{next}
</div>
</div>
"""
) )
H.append("</form>") # but_annee H.append("</form>")
H.append( H.append(
render_template( render_template(
@ -2399,48 +2386,6 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()
def _gen_but_select(
name: str,
codes: list[str],
code_valide: str,
disabled: bool = False,
klass: str = "",
) -> str:
"Le menu html select avec les codes"
h = "\n".join(
[
f"""<option value="{code}"
{'selected' if code == code_valide else ''}
class="{'recorded' if code == code_valide else ''}"
>{code}</option>"""
for code in codes
]
)
return f"""<select required name="{name}"
class="but_code {klass}"
onchange="change_menu_code(this);"
{"disabled" if disabled else ""}
>{h}</select>
"""
def _gen_but_niveau_ue(
ue: UniteEns, moy_ue: float, dec_ue: jury_but.DecisionsProposeesUE
):
return f"""<div class="but_niveau_ue {
'recorded' if dec_ue.code_valide is not None else ''}
">
<div title="{ue.titre}">{ue.acronyme}</div>
<div class="but_note">{scu.fmt_note(moy_ue)}</div>
<div class="but_code">{
_gen_but_select("code_ue_"+str(ue.id),
dec_ue.codes,
dec_ue.code_valide
)
}</div>
</div>"""
@bp.route( @bp.route(
"/formsemestre_validation_auto_but/<int:formsemestre_id>", methods=["GET", "POST"] "/formsemestre_validation_auto_but/<int:formsemestre_id>", methods=["GET", "POST"]
) )
@ -2580,56 +2525,75 @@ def do_formsemestre_validation_auto(formsemestre_id):
def formsemestre_validation_suppress_etud( def formsemestre_validation_suppress_etud(
formsemestre_id, etudid, dialog_confirmed=False formsemestre_id, etudid, dialog_confirmed=False
): ):
"""Suppression des decisions de jury pour un etudiant.""" """Suppression des décisions de jury pour un étudiant."""
if not sco_permissions_check.can_validate_sem(formsemestre_id): if not sco_permissions_check.can_validate_sem(formsemestre_id):
return scu.confirm_dialog( return scu.confirm_dialog(
message="<p>Opération non autorisée pour %s</h2>" % current_user, message="<p>Opération non autorisée pour %s</h2>" % current_user,
dest_url=scu.ScoURL(), dest_url=scu.ScoURL(),
) )
etud = Identite.query.get_or_404(etudid)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc():
next_url = url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
)
else:
next_url = url_for(
"notes.formsemestre_validation_etud_form",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
if not dialog_confirmed: if not dialog_confirmed:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] d = sco_bulletins_json.dict_decision_jury(
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) etudid, formsemestre_id, with_decisions=True
sem = formsemestre.to_dict() )
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) d.update(but_validations.dict_decision_jury(etud, formsemestre))
decision_jury = nt.get_etud_decision_sem(etudid)
if decision_jury: descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])]
existing = ( dec_annee = d.get("decision_annee")
"<p>Décision existante: %(code)s du %(event_date)s</p>" % decision_jury if dec_annee:
) descr_annee = dec_annee.get("code", "-")
else: else:
existing = "" descr_annee = "-"
existing = f"""
<ul>
<li>Semestre : {d.get("decision", {"code":"-"})['code'] or "-"}</li>
<li>Année BUT: {descr_annee}</li>
<li>UEs : {", ".join(descr_ues)}</li>
<li>RCUEs: {len(d.get("decision_rcue", []))} décisions</li>
</ul>
"""
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Confirmer la suppression des décisions du semestre %s (%s - %s) pour %s ?</h2>%s f"""<h2>Confirmer la suppression des décisions du semestre
<p>Cette opération est irréversible. {formsemestre.titre_mois()} pour {etud.nomprenom}
</p> </h2>
""" <p>Cette opération est irréversible.</p>
% ( <div>
sem["titre_num"], {existing}
sem["date_debut"], </div>
sem["date_fin"], """,
etud["nomprenom"],
existing,
),
OK="Supprimer", OK="Supprimer",
dest_url="", dest_url="",
cancel_url="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s" cancel_url=next_url,
% (formsemestre_id, etudid),
parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
) )
sco_formsemestre_validation.formsemestre_validation_suppress_etud( sco_formsemestre_validation.formsemestre_validation_suppress_etud(
formsemestre_id, etudid formsemestre_id, etudid
) )
return flask.redirect( flash("Décisions supprimées")
scu.ScoURL() return flask.redirect(next_url)
+ "/Notes/formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&head_message=Décision%%20supprimée"
% (formsemestre_id, etudid)
)
# ------------- PV de JURY et archives # ------------- PV de JURY et archives
sco_publish("/formsemestre_pvjury", sco_pvjury.formsemestre_pvjury, Permission.ScoView) sco_publish("/formsemestre_pvjury", sco_pvjury.formsemestre_pvjury, Permission.ScoView)
sco_publish("/pvjury_table_but", jury_but_pv.pvjury_table_but, Permission.ScoView)
@bp.route("/formsemestre_saisie_jury") @bp.route("/formsemestre_saisie_jury")
@scodoc @scodoc
@ -2640,18 +2604,18 @@ def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None):
en semestres pairs de BUT, table spécifique avec l'année en semestres pairs de BUT, table spécifique avec l'année
sinon, redirect vers page recap en mode jury sinon, redirect vers page recap en mode jury
""" """
readonly = not sco_permissions_check.can_validate_sem(formsemestre_id) read_only = not sco_permissions_check.can_validate_sem(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0: if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0:
return jury_but_recap.formsemestre_saisie_jury_but( return jury_but_recap.formsemestre_saisie_jury_but(
formsemestre, readonly, selected_etudid=selected_etudid formsemestre, read_only, selected_etudid=selected_etudid
) )
return redirect( return redirect(
url_for( url_for(
"notes.formsemestre_recapcomplet", "notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
modejury=1, mode_jury=1,
) )
) )
@ -2662,14 +2626,14 @@ def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None):
@scodoc7func @scodoc7func
def formsemestre_jury_but_recap(formsemestre_id: int, selected_etudid: int = None): def formsemestre_jury_but_recap(formsemestre_id: int, selected_etudid: int = None):
"""Tableau affichage des codes""" """Tableau affichage des codes"""
readonly = not sco_permissions_check.can_validate_sem(formsemestre_id) read_only = not sco_permissions_check.can_validate_sem(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not (formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0): if not (formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0):
raise ScoValueError( raise ScoValueError(
"formsemestre_jury_but_recap: réservé aux semestres pairs de BUT" "formsemestre_jury_but_recap: réservé aux semestres pairs de BUT"
) )
return jury_but_recap.formsemestre_saisie_jury_but( return jury_but_recap.formsemestre_saisie_jury_but(
formsemestre, readonly=readonly, selected_etudid=selected_etudid, mode="recap" formsemestre, read_only=read_only, selected_etudid=selected_etudid, mode="recap"
) )

0
bench.py Normal file → Executable file
View File

0
pylintrc Normal file → Executable file
View File

2
sco_version.py Normal file → Executable file
View File

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

View File

@ -33,7 +33,7 @@ except NameError:
load_dotenv(os.path.join(BASEDIR, ".env")) load_dotenv(os.path.join(BASEDIR, ".env"))
CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
SCODOC_URL = os.environ["SCODOC_URL"] or "http://localhost:5000" SCODOC_URL = os.environ.get("SCODOC_URL") or "http://localhost:5000"
API_URL = SCODOC_URL + "/ScoDoc/api" API_URL = SCODOC_URL + "/ScoDoc/api"
SCODOC_USER = os.environ["SCODOC_USER"] SCODOC_USER = os.environ["SCODOC_USER"]
SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"] SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"]
@ -85,13 +85,13 @@ if r.status_code != 200:
print(f"{len(r.json())} étudiants courants") print(f"{len(r.json())} étudiants courants")
# Bulletin d'un BUT # Bulletin d'un BUT
formsemestre_id = 1052 # A adapter formsemestre_id = 1063 # A adapter
etudid = 16400 etudid = 16450
bul = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") bul = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin")
# d'un DUT # d'un DUT
formsemestre_id = 1028 # A adapter formsemestre_id = 1062 # A adapter
etudid = 14721 etudid = 16309
bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin")

View File

@ -21,14 +21,36 @@ import requests
from app.api.formsemestres import formsemestre from app.api.formsemestres import formsemestre
from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
from tests.api.tools_test_api import ( from tests.api.tools_test_api import (
MODIMPL_FIELDS,
verify_fields, verify_fields,
MODIMPL_FIELDS,
EVAL_FIELDS, EVAL_FIELDS,
SAISIE_NOTES_FIELDS, SAISIE_NOTES_FIELDS,
FORMSEMESTRE_ETUS_FIELDS, FORMSEMESTRE_ETUS_FIELDS,
FSEM_FIELDS,
FSEM_FIELDS,
UE_FIELDS,
MODULE_FIELDS,
FORMSEMESTRE_BULLETINS_FIELDS,
FORMSEMESTRE_BULLETINS_ETU_FIELDS,
FORMSEMESTRE_BULLETINS_FORMATION_FIELDS,
FORMSEMESTRE_BULLETINS_OPT_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS,
BULLETIN_UES_UE_FIELDS,
BULLETIN_UES_UE_MOYENNE_FIELDS,
BULLETIN_UES_UE_RESSOURCES_RESSOURCE_FIELDS,
BULLETIN_UES_UE_SAES_SAE_FIELDS,
BULLETIN_UES_UE_ECTS_FIELDS,
BULLETIN_SEMESTRE_FIELDS,
BULLETIN_SEMESTRE_ABSENCES_FIELDS,
BULLETIN_SEMESTRE_ECTS_FIELDS,
BULLETIN_SEMESTRE_NOTES_FIELDS,
BULLETIN_SEMESTRE_RANG_FIELDS,
) )
from tests.api.tools_test_api import FSEM_FIELDS, UE_FIELDS, MODULE_FIELDS
# Etudiant pour les tests # Etudiant pour les tests
ETUDID = 1 ETUDID = 1
@ -143,26 +165,318 @@ def test_formsemestre_apo(api_headers):
assert isinstance(formsemestre["titre"], str) assert isinstance(formsemestre["titre"], str)
### ERROR ### ### ERROR ###
etape_apo_inexistante = "aoefiaozidaoẑidjnoaiznjd" # etape_apo_inexistante = "aoefiaozidaoẑidjnoaiznjd"
r_error = requests.get( # r_error = requests.get(
f"{API_URL}/formsemestre/apo/{etape_apo_inexistante}", # f"{API_URL}/formsemestre/apo/{etape_apo_inexistante}",
headers=api_headers, # headers=api_headers,
verify=CHECK_CERTIFICATE, # verify=CHECK_CERTIFICATE,
) # )
assert r_error.status_code == 404 # assert r_error.status_code == 404
def test_bulletins(api_headers): def test_bulletins(api_headers):
""" """
Route: /formsemestre/<int:formsemestre_id>/bulletins Route: /formsemestre/<int:formsemestre_id>/bulletins
""" """
formsemestre_id = 1
r = requests.get( r = requests.get(
API_URL + "/formsemestre/1/bulletins", f"{API_URL}/formsemestre/{formsemestre_id}/bulletins",
headers=api_headers, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
bulletins = r.json()
assert isinstance(bulletins, list)
for bul in bulletins:
assert verify_fields(bul, FORMSEMESTRE_BULLETINS_FIELDS) is True
assert isinstance(bul["version"], str)
assert isinstance(bul["type"], str)
assert isinstance(bul["date"], str)
assert isinstance(bul["publie"], bool)
assert isinstance(bul["etudiant"], dict)
assert isinstance(bul["formation"], dict)
assert isinstance(bul["formsemestre_id"], int)
assert isinstance(bul["etat_inscription"], str)
assert isinstance(bul["options"], dict)
assert isinstance(bul["ressources"], dict)
assert isinstance(bul["saes"], dict)
assert isinstance(bul["ues"], dict)
assert isinstance(bul["semestre"], dict)
formsemestre_id_bul = bul["formsemestre_id"]
assert formsemestre_id == formsemestre_id_bul
etudiant = bul["etudiant"]
assert verify_fields(etudiant, FORMSEMESTRE_BULLETINS_ETU_FIELDS) is True
assert isinstance(etudiant["civilite"], str)
assert isinstance(etudiant["code_ine"], str)
assert isinstance(etudiant["code_nip"], str)
assert isinstance(etudiant["date_naissance"], str)
assert isinstance(etudiant["dept_id"], int)
assert isinstance(etudiant["dept_acronym"], str)
assert isinstance(etudiant["email"], str)
assert isinstance(etudiant["emailperso"], str)
assert isinstance(etudiant["etudid"], int)
assert isinstance(etudiant["nom"], str)
assert isinstance(etudiant["prenom"], str)
assert isinstance(etudiant["nomprenom"], str)
assert isinstance(etudiant["lieu_naissance"], str)
assert isinstance(etudiant["dept_naissance"], str)
assert isinstance(etudiant["nationalite"], str)
assert isinstance(etudiant["boursier"], str)
assert isinstance(etudiant["fiche_url"], str)
assert isinstance(etudiant["photo_url"], str)
assert isinstance(etudiant["id"], int)
assert isinstance(etudiant["codepostaldomicile"], str)
assert isinstance(etudiant["paysdomicile"], str)
assert isinstance(etudiant["telephonemobile"], str)
assert isinstance(etudiant["typeadresse"], str)
assert isinstance(etudiant["domicile"], str)
assert isinstance(etudiant["villedomicile"], str)
assert isinstance(etudiant["telephone"], str)
assert isinstance(etudiant["fax"], str)
assert isinstance(etudiant["description"], str)
formation = bul["formation"]
assert verify_fields(formation, FORMSEMESTRE_BULLETINS_FORMATION_FIELDS) is True
assert isinstance(formation["id"], int)
assert isinstance(formation["acronyme"], str)
assert isinstance(formation["titre_officiel"], str)
assert isinstance(formation["titre"], str)
options = bul["options"]
assert verify_fields(options, FORMSEMESTRE_BULLETINS_OPT_FIELDS) is True
assert isinstance(options["show_abs"], bool)
assert isinstance(options["show_abs_modules"], bool)
assert isinstance(options["show_ects"], bool)
assert isinstance(options["show_codemodules"], bool)
assert isinstance(options["show_matieres"], bool)
assert isinstance(options["show_rangs"], bool)
assert isinstance(options["show_ue_rangs"], bool)
assert isinstance(options["show_mod_rangs"], bool)
assert isinstance(options["show_moypromo"], bool)
assert isinstance(options["show_minmax"], bool)
assert isinstance(options["show_minmax_mod"], bool)
assert isinstance(options["show_minmax_eval"], bool)
assert isinstance(options["show_coef"], bool)
assert isinstance(options["show_ue_cap_details"], bool)
assert isinstance(options["show_ue_cap_current"], bool)
assert isinstance(options["show_temporary"], bool)
assert isinstance(options["temporary_txt"], str)
assert isinstance(options["show_uevalid"], bool)
assert isinstance(options["show_date_inscr"], bool)
bulletin_ressources = bul["ressources"]
assert isinstance(bulletin_ressources, dict)
for ressource in bulletin_ressources.values():
assert (
verify_fields(
ressource, BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS
)
is True
)
assert isinstance(ressource, dict)
assert isinstance(ressource["evaluations"], list)
for evaluation in ressource["evaluations"]:
assert (
verify_fields(
evaluation,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS,
)
is True
)
assert isinstance(evaluation["id"], int)
assert evaluation["description"] is None or isinstance(
evaluation["description"], str
)
assert evaluation["date"] is None or isinstance(evaluation["date"], str)
assert isinstance(evaluation["heure_debut"], str)
assert isinstance(evaluation["heure_fin"], str)
assert isinstance(evaluation["coef"], str)
assert isinstance(evaluation["poids"], dict)
assert isinstance(evaluation["note"], dict)
assert isinstance(evaluation["url"], str)
assert (
verify_fields(
evaluation["poids"],
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS,
)
is True
)
assert isinstance(evaluation["poids"]["RT1.1"], float)
assert isinstance(evaluation["poids"]["RT2.1"], float)
assert isinstance(evaluation["poids"]["RT3.1"], float)
assert (
verify_fields(
evaluation["note"],
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS,
)
is True
)
assert isinstance(evaluation["note"]["value"], str)
assert isinstance(evaluation["note"]["min"], str)
assert isinstance(evaluation["note"]["max"], str)
assert isinstance(evaluation["note"]["moy"], str)
bulletin_saes = bul["saes"]
assert isinstance(bulletin_saes, dict)
for sae in bulletin_saes.values():
assert (
verify_fields(sae, BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS)
is True
)
assert isinstance(sae, dict)
assert isinstance(sae["evaluations"], list)
for evaluation in sae["evaluations"]:
assert (
verify_fields(
evaluation,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS,
)
is True
)
assert isinstance(evaluation["id"], int)
assert evaluation["description"] is None or isinstance(
evaluation["description"], str
)
assert evaluation["date"] is None or isinstance(evaluation["date"], str)
assert isinstance(evaluation["heure_debut"], str)
assert isinstance(evaluation["heure_fin"], str)
assert isinstance(evaluation["coef"], str)
assert isinstance(evaluation["poids"], dict)
assert isinstance(evaluation["note"], dict)
assert isinstance(evaluation["url"], str)
assert (
verify_fields(
evaluation["poids"],
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS,
)
is True
)
assert isinstance(evaluation["poids"]["RT1.1"], float)
assert isinstance(evaluation["poids"]["RT2.1"], float)
assert isinstance(evaluation["poids"]["RT3.1"], float)
assert (
verify_fields(
evaluation["note"],
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS,
)
is True
)
assert isinstance(evaluation["note"]["value"], str)
assert isinstance(evaluation["note"]["min"], str)
assert isinstance(evaluation["note"]["max"], str)
assert isinstance(evaluation["note"]["moy"], str)
bulletin_ues = bul["ues"]
assert isinstance(bulletin_ues, dict)
for (key_ue, value_ue) in bulletin_ues.items():
assert verify_fields(value_ue, BULLETIN_UES_UE_FIELDS) is True
assert isinstance(value_ue["id"], int)
assert isinstance(value_ue["titre"], str)
assert isinstance(value_ue["numero"], int)
assert isinstance(value_ue["type"], int)
assert isinstance(value_ue["color"], str)
assert value_ue["competence"] is None or isinstance(
value_ue["competence"], str
)
assert isinstance(value_ue["moyenne"], dict)
assert isinstance(value_ue["bonus"], str)
assert isinstance(value_ue["malus"], str)
assert value_ue["capitalise"] is None or isinstance(
value_ue["capitalise"], str
)
assert isinstance(value_ue["ressources"], dict)
assert isinstance(value_ue["saes"], dict)
assert isinstance(value_ue["ECTS"], dict)
assert (
verify_fields(value_ue["moyenne"], BULLETIN_UES_UE_MOYENNE_FIELDS)
is True
)
assert isinstance(value_ue["moyenne"]["value"], str)
assert isinstance(value_ue["moyenne"]["min"], str)
assert isinstance(value_ue["moyenne"]["max"], str)
assert isinstance(value_ue["moyenne"]["moy"], str)
assert isinstance(value_ue["moyenne"]["rang"], str)
assert isinstance(value_ue["moyenne"]["total"], int)
for ressource in value_ue["ressources"].values():
assert (
verify_fields(
ressource, BULLETIN_UES_UE_RESSOURCES_RESSOURCE_FIELDS
)
is True
)
assert isinstance(ressource["id"], int)
assert isinstance(ressource["coef"], float)
assert isinstance(ressource["moyenne"], str)
for sae in value_ue["saes"].values():
assert verify_fields(sae, BULLETIN_UES_UE_SAES_SAE_FIELDS) is True
assert isinstance(sae["id"], int)
assert isinstance(sae["coef"], float)
assert isinstance(sae["moyenne"], str)
assert verify_fields(value_ue["ECTS"], BULLETIN_UES_UE_ECTS_FIELDS) is True
assert isinstance(value_ue["ECTS"]["acquis"], float)
assert isinstance(value_ue["ECTS"]["total"], float)
bulletin_semestre = bul["semestre"]
assert verify_fields(bulletin_semestre, BULLETIN_SEMESTRE_FIELDS) is True
assert isinstance(bulletin_semestre["etapes"], list)
assert isinstance(bulletin_semestre["date_debut"], str)
assert isinstance(bulletin_semestre["date_fin"], str)
assert isinstance(bulletin_semestre["annee_universitaire"], str)
assert isinstance(bulletin_semestre["numero"], int)
assert isinstance(bulletin_semestre["inscription"], str)
assert isinstance(bulletin_semestre["groupes"], list)
assert isinstance(bulletin_semestre["absences"], dict)
assert isinstance(bulletin_semestre["ECTS"], dict)
assert isinstance(bulletin_semestre["notes"], dict)
assert isinstance(bulletin_semestre["rang"], dict)
assert (
verify_fields(
bulletin_semestre["absences"], BULLETIN_SEMESTRE_ABSENCES_FIELDS
)
is True
)
assert isinstance(bulletin_semestre["absences"]["injustifie"], int)
assert isinstance(bulletin_semestre["absences"]["total"], int)
assert (
verify_fields(bulletin_semestre["ECTS"], BULLETIN_SEMESTRE_ECTS_FIELDS)
is True
)
assert isinstance(bulletin_semestre["ECTS"]["acquis"], int)
assert isinstance(bulletin_semestre["ECTS"]["total"], float)
assert (
verify_fields(bulletin_semestre["notes"], BULLETIN_SEMESTRE_NOTES_FIELDS)
is True
)
assert isinstance(bulletin_semestre["notes"]["value"], str)
assert isinstance(bulletin_semestre["notes"]["min"], str)
assert isinstance(bulletin_semestre["notes"]["max"], str)
assert isinstance(bulletin_semestre["notes"]["moy"], str)
assert (
verify_fields(bulletin_semestre["rang"], BULLETIN_SEMESTRE_RANG_FIELDS)
is True
)
assert isinstance(bulletin_semestre["rang"]["value"], str)
assert isinstance(bulletin_semestre["rang"]["total"], int)
# # jury # # jury
# def test_jury(): # def test_jury():

View File

@ -641,3 +641,79 @@ PARTITIONS_GROUPS_ETU_FIELDS = {
"ne", "ne",
"email_default", "email_default",
} }
FORMSEMESTRE_BULLETINS_FIELDS = {
"version",
"type",
"date",
"publie",
"etudiant",
"formation",
"formsemestre_id",
"etat_inscription",
"options",
"ressources",
"saes",
"ues",
"semestre",
}
FORMSEMESTRE_BULLETINS_ETU_FIELDS = {
"civilite",
"code_ine",
"code_nip",
"date_naissance",
"dept_id",
"dept_acronym",
"email",
"emailperso",
"etudid",
"nom",
"prenom",
"nomprenom",
"lieu_naissance",
"dept_naissance",
"nationalite",
"boursier",
"fiche_url",
"photo_url",
"id",
"codepostaldomicile",
"paysdomicile",
"telephonemobile",
"typeadresse",
"domicile",
"villedomicile",
"telephone",
"fax",
"description",
}
FORMSEMESTRE_BULLETINS_FORMATION_FIELDS = {
"id",
"acronyme",
"titre_officiel",
"titre",
}
FORMSEMESTRE_BULLETINS_OPT_FIELDS = {
"show_abs",
"show_abs_modules",
"show_ects",
"show_codemodules",
"show_matieres",
"show_rangs",
"show_ue_rangs",
"show_mod_rangs",
"show_moypromo",
"show_minmax",
"show_minmax_mod",
"show_minmax_eval",
"show_coef",
"show_ue_cap_details",
"show_ue_cap_current",
"show_temporary",
"temporary_txt",
"show_uevalid",
"show_date_inscr",
}