Merge branch 'new_api' of https://scodoc.org/git/leonard.montalbano/ScoDoc into new_api
This commit is contained in:
commit
e822ab4da0
@ -212,7 +212,9 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
|
||||
|
||||
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(
|
||||
@ -471,7 +473,7 @@ def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner
|
||||
return response
|
||||
|
||||
return sco_bulletins.get_formsemestre_bulletin_etud_json(
|
||||
formsemestre, etud, version
|
||||
formsemestre, etud, version=version
|
||||
)
|
||||
|
||||
|
||||
|
@ -62,7 +62,7 @@ def formsemestre(formsemestre_id: int):
|
||||
formsemestre: FormSemestre = models.FormSemestre.query.filter_by(
|
||||
id=formsemestre_id
|
||||
).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 accéder aux préferences
|
||||
dept = Departement.query.get(formsemestre.dept_id)
|
||||
@ -92,13 +92,9 @@ def formsemestre_apo(etape_apo: str):
|
||||
FormSemestreEtape.formsemestre_id == FormSemestre.id,
|
||||
)
|
||||
|
||||
res = [formsemestre.to_dict() for formsemestre in formsemestres]
|
||||
if len(res) == 0:
|
||||
return error_response(
|
||||
404, message="Aucun formsemestre trouvé avec cette étape apogée"
|
||||
)
|
||||
else:
|
||||
return jsonify(res)
|
||||
return jsonify(
|
||||
[formsemestre.to_dict(convert_parcours=True) for formsemestre in formsemestres]
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"])
|
||||
|
@ -14,6 +14,7 @@ from flask import url_for, g
|
||||
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.models import but_validations
|
||||
from app.models.groups import GroupDescr
|
||||
from app.models.ues import UniteEns
|
||||
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_acquis = sum([d.get("ects", 0) for d in decisions_ues.values()])
|
||||
semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
|
||||
semestre_infos.update(
|
||||
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
|
||||
)
|
||||
if sco_preferences.get_preference("bul_show_decision", 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:
|
||||
# moyenne des moyennes générales du semestre
|
||||
semestre_infos["notes"] = {
|
||||
|
@ -68,7 +68,7 @@ from flask import g, url_for
|
||||
from app import db
|
||||
from app import log
|
||||
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.but_refcomp import (
|
||||
@ -189,6 +189,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
sco_codes.RAT,
|
||||
sco_codes.ABAN,
|
||||
sco_codes.ABL,
|
||||
sco_codes.ATJ,
|
||||
sco_codes.DEF,
|
||||
sco_codes.DEM,
|
||||
sco_codes.EXCLU,
|
||||
@ -200,6 +201,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
formsemestre: FormSemestre,
|
||||
):
|
||||
super().__init__(etud=etud)
|
||||
self.formsemestre_id = formsemestre.id
|
||||
formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre)
|
||||
assert (
|
||||
(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)"
|
||||
self.formsemestre_pair = formsemestre_pair
|
||||
"le second formsemestre de la même année scolaire (S2, S4, S6)"
|
||||
self.annee_but = (
|
||||
formsemestre_impair.semestre_id // 2 + 1
|
||||
if formsemestre_impair
|
||||
else formsemestre_pair.semestre_id // 2
|
||||
)
|
||||
formsemestre_last = formsemestre_pair or formsemestre_impair
|
||||
"le formsemestre le plus avancé dans cette année"
|
||||
|
||||
self.annee_but = (formsemestre_last.semestre_id + 1) // 2
|
||||
"le rang de l'année dans le BUT: 1, 2, 3"
|
||||
assert self.annee_but in (1, 2, 3)
|
||||
self.rcues_annee = []
|
||||
"RCUEs de l'année"
|
||||
self.inscription_etat = etud.inscription_etat(formsemestre_last.id)
|
||||
|
||||
if self.formsemestre_impair is not None:
|
||||
self.validation = ApcValidationAnnee.query.filter_by(
|
||||
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.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
|
||||
}
|
||||
"{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année"
|
||||
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
|
||||
}
|
||||
)
|
||||
@ -289,8 +296,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
[rcue for rcue in rcues_avec_niveau if not rcue.est_suffisant()]
|
||||
)
|
||||
"le nb de comp. sous la barre de 8/20"
|
||||
# année ADM si toutes RCUE validées (sinon PASD)
|
||||
self.admis = self.nb_validables == self.nb_competences
|
||||
# année ADM si toutes RCUE validées (sinon PASD) et non DEM ou DEF
|
||||
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"
|
||||
self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2)
|
||||
# Peut passer si plus de la moitié validables et tous > 8
|
||||
@ -308,6 +317,19 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
if self.admis:
|
||||
self.codes = [sco_codes.ADM] + self.codes
|
||||
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:
|
||||
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
|
||||
self.explanation = expl_rcues
|
||||
@ -385,7 +407,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
def comp_formsemestres(
|
||||
self, 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:
|
||||
other_semestre_id = formsemestre.semestre_id - 1
|
||||
else:
|
||||
@ -419,7 +443,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
(self.formsemestre_impair, self.res_impair),
|
||||
(self.formsemestre_pair, self.res_pair),
|
||||
):
|
||||
if formsemestre is None:
|
||||
if (formsemestre is None) or (not formsemestre.formation.is_apc()):
|
||||
ues = []
|
||||
else:
|
||||
formation: Formation = formsemestre.formation
|
||||
@ -478,6 +502,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
ue_impair,
|
||||
self.formsemestre_pair,
|
||||
ue_pair,
|
||||
self.inscription_etat,
|
||||
)
|
||||
ues_impair_sans_rcue.discard(ue_impair.id)
|
||||
break
|
||||
@ -505,7 +530,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
rcue = rc
|
||||
break
|
||||
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))
|
||||
# prévient les UE concernées :-)
|
||||
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.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):
|
||||
"""Liste des codes de décisions que l'on peut proposer pour
|
||||
@ -673,20 +738,33 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
|
||||
codes_communs = [
|
||||
sco_codes.ADJ,
|
||||
sco_codes.ATJ,
|
||||
sco_codes.RAT,
|
||||
sco_codes.DEF,
|
||||
sco_codes.ABAN,
|
||||
]
|
||||
|
||||
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)
|
||||
self.rcue = rcue
|
||||
if rcue is None: # RCUE non dispo, eg un seul semestre
|
||||
self.codes = []
|
||||
return
|
||||
self.inscription_etat = inscription_etat
|
||||
"inscription: I, DEM, DEF"
|
||||
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()
|
||||
if self.validation is not None:
|
||||
self.code_valide = self.validation.code
|
||||
@ -737,6 +815,21 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
db.session.delete(validation)
|
||||
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):
|
||||
"""Décisions de jury sur une UE du BUT
|
||||
@ -758,6 +851,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
sco_codes.RAT,
|
||||
sco_codes.DEF,
|
||||
sco_codes.ABAN,
|
||||
sco_codes.ATJ,
|
||||
sco_codes.DEM,
|
||||
sco_codes.UEBSL,
|
||||
]
|
||||
@ -767,12 +861,27 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
etud: Identite,
|
||||
formsemestre: FormSemestre,
|
||||
ue: UniteEns,
|
||||
inscription_etat: str = scu.INSCRIT,
|
||||
):
|
||||
super().__init__(etud=etud)
|
||||
self.formsemestre = formsemestre
|
||||
self.ue: UniteEns = ue
|
||||
self.rcue: RegroupementCoherentUE = 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)
|
||||
# mais ici on a restreint au formsemestre donc une seule (prend la première)
|
||||
self.validation = ScolarFormSemestreValidation.query.filter_by(
|
||||
@ -780,10 +889,6 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
).first()
|
||||
if self.validation is not None:
|
||||
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 ?
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
@ -802,6 +907,8 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
|
||||
def compute_codes(self):
|
||||
"""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):
|
||||
self.codes.insert(0, sco_codes.ADM)
|
||||
self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",)
|
||||
@ -853,6 +960,14 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
db.session.delete(validation)
|
||||
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
|
||||
"""Validation du cursus d'un étudiant"""
|
||||
@ -932,7 +1047,7 @@ class BUTCursusEtud: # WIP TODO
|
||||
"""La liste des UE à valider si on valide ce niveau.
|
||||
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 d’une compétence emporte la validation de
|
||||
l'ensemble des UE du niveau inférieur de cette même compétence.
|
||||
"""
|
||||
|
137
app/but/jury_but_pv.py
Normal file
137
app/but/jury_but_pv.py
Normal 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)
|
@ -36,7 +36,7 @@ from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
def formsemestre_saisie_jury_but(
|
||||
formsemestre2: FormSemestre,
|
||||
readonly: bool = False,
|
||||
read_only: bool = False,
|
||||
selected_etudid: int = None,
|
||||
mode="jury",
|
||||
) -> str:
|
||||
@ -72,7 +72,7 @@ def formsemestre_saisie_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:
|
||||
return (
|
||||
@ -98,7 +98,16 @@ def formsemestre_saisie_jury_but(
|
||||
]
|
||||
if mode == "recap":
|
||||
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(
|
||||
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(
|
||||
f"""
|
||||
<p><a class="stdlink" href="{url_for(
|
||||
@ -333,6 +342,10 @@ class RowCollector:
|
||||
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
||||
"col_rcue col_rcues_validables" + klass,
|
||||
)
|
||||
self["_rcues_validables_data"] = {
|
||||
"etudid": deca.etud.id,
|
||||
"nomprenom": deca.etud.nomprenom,
|
||||
}
|
||||
if len(deca.rcues_annee) > 0:
|
||||
# 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:
|
||||
@ -353,7 +366,7 @@ class RowCollector:
|
||||
|
||||
|
||||
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]]:
|
||||
"""Construit la table des résultats annuels pour le jury BUT"""
|
||||
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
|
||||
@ -383,7 +396,7 @@ def get_table_jury_but(
|
||||
"col_code_annee",
|
||||
)
|
||||
# --- Le lien de saisie
|
||||
if not readonly and not mode == "recap":
|
||||
if mode != "recap":
|
||||
row.add_cell(
|
||||
"lien_saisie",
|
||||
"",
|
||||
@ -394,9 +407,11 @@ def get_table_jury_but(
|
||||
etudid=etud.id,
|
||||
formsemestre_id=formsemestre2.id,
|
||||
)}" class="stdlink">
|
||||
{"modif." if deca.code_valide else "saisie"}
|
||||
{"voir" if read_only else ("modif." if deca.code_valide else "saisie")}
|
||||
décision</a>
|
||||
""",
|
||||
"""
|
||||
if deca.inscription_etat == scu.INSCRIT
|
||||
else deca.inscription_etat,
|
||||
"col_lien_saisie_but",
|
||||
)
|
||||
rows.append(row)
|
||||
|
173
app/but/jury_but_view.py
Normal file
173
app/but/jury_but_view.py
Normal 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 ""
|
@ -496,17 +496,26 @@ def compute_malus(
|
||||
"""
|
||||
ues_idx = [ue.id for ue in ues]
|
||||
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:
|
||||
if ue.type != UE_SPORT:
|
||||
modimpl_mask = np.array(
|
||||
[
|
||||
(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
|
||||
]
|
||||
)
|
||||
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.fillna(0.0, inplace=True)
|
||||
|
@ -399,7 +399,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
# --- TABLEAU 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
|
||||
- rows: liste de dicts { column_id : value }
|
||||
@ -550,7 +550,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
titles_bot[
|
||||
f"_{col_id}_target_attrs"
|
||||
] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
|
||||
if modejury:
|
||||
if mode_jury:
|
||||
# pas d'autre colonnes de résultats
|
||||
continue
|
||||
# Bonus (sport) dans cette UE ?
|
||||
@ -650,7 +650,17 @@ class ResultatsSemestre(ResultatsCache):
|
||||
elif nb_ues_validables < len(ues_sans_bonus):
|
||||
row["_ues_validables_class"] += " moy_inf"
|
||||
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(
|
||||
row,
|
||||
"jury_link",
|
||||
@ -660,7 +670,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
)
|
||||
}">saisir décision</a>""",
|
||||
"col_jury_link",
|
||||
1000,
|
||||
idx,
|
||||
)
|
||||
rows.append(row)
|
||||
|
||||
|
@ -54,6 +54,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
|
||||
self.expr_diagnostics = ""
|
||||
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]:
|
||||
"""Liste des étudiants inscrits
|
||||
@ -145,6 +146,10 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
"""Liste des modules pour une UE (ou toutes si ue_id==None),
|
||||
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 = []
|
||||
for modimpl in self.formsemestre.modimpls_sorted:
|
||||
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
|
||||
d["mat"] = modimpl.module.matiere.to_dict()
|
||||
modimpls_dict.append(d)
|
||||
self._modimpls_dict_by_ue[ue_id] = modimpls_dict
|
||||
return modimpls_dict
|
||||
|
||||
def compute_rangs(self):
|
||||
|
@ -43,7 +43,7 @@ from app.scodoc import sco_logos, html_sco_header
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
from app.scodoc.sco_config_actions import LogoInsert
|
||||
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
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 validate_logo_name(form, field):
|
||||
name = field.data if field.data else ""
|
||||
if "." in name:
|
||||
raise ValidationError(message)
|
||||
if not scu.is_valid_filename(name):
|
||||
raise ValidationError(message)
|
||||
|
||||
@ -199,9 +201,12 @@ class LogoForm(FlaskForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["meta"] = {"csrf": False}
|
||||
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)
|
||||
).select()
|
||||
)
|
||||
if logo is None:
|
||||
raise ScoValueError("logo introuvable")
|
||||
self.logo = logo.select()
|
||||
self.description = None
|
||||
self.titre = None
|
||||
self.can_delete = True
|
||||
|
@ -255,6 +255,7 @@ class ApcCompetence(db.Model, XMLModel):
|
||||
return f"<ApcCompetence {self.id} {self.titre!r}>"
|
||||
|
||||
def to_dict(self):
|
||||
"repr dict recursive sur situations, composantes, niveaux"
|
||||
return {
|
||||
"id_orebut": self.id_orebut,
|
||||
"titre": self.titre,
|
||||
@ -268,6 +269,16 @@ class ApcCompetence(db.Model, XMLModel):
|
||||
"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):
|
||||
"Situation professionnelle"
|
||||
@ -341,6 +352,7 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
self.annee!r} {self.competence!r}>"""
|
||||
|
||||
def to_dict(self):
|
||||
"as a dict, recursif sur les AC"
|
||||
return {
|
||||
"libelle": self.libelle,
|
||||
"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},
|
||||
}
|
||||
|
||||
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
|
||||
def niveaux_annee_de_parcours(
|
||||
cls,
|
||||
@ -430,6 +451,7 @@ class ApcAppCritique(db.Model, XMLModel):
|
||||
if competence is not None:
|
||||
query = query.filter(ApcNiveau.competence == competence)
|
||||
return query
|
||||
<<<<<<< HEAD
|
||||
|
||||
def __init__(self, id, niveau_id, code, libelle, modules):
|
||||
self.id = id
|
||||
@ -437,6 +459,8 @@ class ApcAppCritique(db.Model, XMLModel):
|
||||
self.code = code
|
||||
self.libelle = libelle
|
||||
self.modules = modules
|
||||
=======
|
||||
>>>>>>> 7c340c798ad59c41653efc83bfd079f11fce1938
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"libelle": self.libelle}
|
||||
@ -523,11 +547,14 @@ class ApcAnneeParcours(db.Model, XMLModel):
|
||||
)
|
||||
ordre = db.Column(db.Integer)
|
||||
"numéro de l'année: 1, 2, 3"
|
||||
<<<<<<< HEAD
|
||||
|
||||
def __init__(self, id, parcours_id, ordre):
|
||||
self.id = id
|
||||
self.parcours_id = parcours_id
|
||||
self.ordre = ordre
|
||||
=======
|
||||
>>>>>>> 7c340c798ad59c41653efc83bfd079f11fce1938
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} ordre={self.ordre!r} parcours={self.parcours.code!r}>"
|
||||
|
@ -13,8 +13,10 @@ from app.models import CODE_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_codes_parcours as sco_codes
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class ApcValidationRCUE(db.Model):
|
||||
@ -41,6 +43,7 @@ class ApcValidationRCUE(db.Model):
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
||||
)
|
||||
"formsemestre pair du RCUE"
|
||||
# Les deux UE associées à ce niveau:
|
||||
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)
|
||||
@ -63,6 +66,10 @@ class ApcValidationRCUE(db.Model):
|
||||
# Par convention, il est donné par la seconde UE
|
||||
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:
|
||||
class RegroupementCoherentUE:
|
||||
@ -79,6 +86,7 @@ class RegroupementCoherentUE:
|
||||
ue_1: UniteEns,
|
||||
formsemestre_2: FormSemestre,
|
||||
ue_2: UniteEns,
|
||||
inscription_etat: str,
|
||||
):
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
@ -104,6 +112,11 @@ class RegroupementCoherentUE:
|
||||
"semestre pair"
|
||||
self.ue_2 = ue_2
|
||||
# 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)
|
||||
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]
|
||||
@ -190,14 +203,15 @@ class RegroupementCoherentUE:
|
||||
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
|
||||
validation = self.query_validations().first()
|
||||
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 None
|
||||
|
||||
|
||||
# unused
|
||||
def find_rcues(
|
||||
formsemestre: FormSemestre, ue: UniteEns, etud: Identite
|
||||
formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
|
||||
) -> list[RegroupementCoherentUE]:
|
||||
"""Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
||||
ce semestre pour cette UE.
|
||||
@ -245,7 +259,9 @@ def find_rcues(
|
||||
other_ue = UniteEns.query.get(ue_id)
|
||||
other_formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
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é:
|
||||
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||
@ -280,3 +296,45 @@ class ApcValidationAnnee(db.Model):
|
||||
|
||||
def __repr__(self):
|
||||
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
|
||||
|
@ -26,6 +26,7 @@ from app.scodoc.sco_codes_parcours import (
|
||||
PASD,
|
||||
PAS1NCI,
|
||||
RAT,
|
||||
RED,
|
||||
)
|
||||
|
||||
CODES_SCODOC_TO_APO = {
|
||||
@ -46,6 +47,7 @@ CODES_SCODOC_TO_APO = {
|
||||
PASD: "PASD",
|
||||
PAS1NCI: "PAS1NCI",
|
||||
RAT: "ATT",
|
||||
RED: "RED",
|
||||
"NOTES_FMT": "%3.2f",
|
||||
}
|
||||
|
||||
|
@ -136,9 +136,9 @@ class Identite(db.Model):
|
||||
"clé pour tris par ordre alphabétique"
|
||||
return (
|
||||
scu.sanitize_string(
|
||||
scu.suppress_accents(self.nom_usuel or self.nom or "").lower()
|
||||
),
|
||||
scu.sanitize_string(scu.suppress_accents(self.prenom or "").lower()),
|
||||
self.nom_usuel or self.nom or "", remove_spaces=False
|
||||
).lower(),
|
||||
scu.sanitize_string(self.prenom or "", remove_spaces=False).lower(),
|
||||
)
|
||||
|
||||
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))
|
||||
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):
|
||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||
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
|
||||
|
||||
def inscriptions_courantes(self) -> list: # -> list[FormSemestreInscription]:
|
||||
def inscriptions_courantes(self) -> list["FormSemestreInscription"]:
|
||||
"""Liste des inscriptions à des semestres _courants_
|
||||
(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).
|
||||
@ -244,18 +257,6 @@ class Identite(db.Model):
|
||||
]
|
||||
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:
|
||||
"""Description de l'état d'inscription"""
|
||||
inscription_courante = self.inscription_courante()
|
||||
@ -294,6 +295,18 @@ class Identite(db.Model):
|
||||
"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:
|
||||
"""Chaîne décrivant la situation _actuelle_ de l'étudiant.
|
||||
Exemple:
|
||||
@ -365,6 +378,15 @@ class Identite(db.Model):
|
||||
|
||||
return situation
|
||||
|
||||
def etat_civil_pv(self, line_sep="\n") -> str:
|
||||
"""Présentation, pour PV jury
|
||||
M. Pierre Dupont
|
||||
n° 12345678
|
||||
né(e) le 7/06/1974
|
||||
à Paris
|
||||
"""
|
||||
return f"""{self.nomprenom}{line_sep}n°{self.code_nip or ""}{line_sep}né{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:
|
||||
"""HTML img tag for the photo, either in small size (h90)
|
||||
or original size (size=="orig")
|
||||
|
@ -141,7 +141,7 @@ class FormSemestre(db.Model):
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self, convert_parcours=False):
|
||||
"dict (compatible ScoDoc7)"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
@ -160,6 +160,8 @@ class FormSemestre(db.Model):
|
||||
d["date_fin"] = d["date_fin_iso"] = ""
|
||||
d["responsables"] = [u.id for u in self.responsables]
|
||||
d["titre_formation"] = self.titre_formation()
|
||||
if convert_parcours:
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
return d
|
||||
|
||||
def to_dict_api(self):
|
||||
@ -507,6 +509,19 @@ class FormSemestre(db.Model):
|
||||
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]:
|
||||
"""Liste des étudiants inscrits à ce semestre
|
||||
Si include_demdef, tous les étudiants, avec les démissionnaires
|
||||
|
@ -175,6 +175,12 @@ class Module(db.Model):
|
||||
# Liste seulement les coefs définis:
|
||||
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):
|
||||
"""Coefficients des modules vers les UE (APC, BUT)
|
||||
|
@ -120,3 +120,9 @@ class UniteEns(db.Model):
|
||||
(Module.module_type != scu.ModuleType.SAE),
|
||||
(Module.module_type != scu.ModuleType.RESSOURCE),
|
||||
).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()
|
||||
|
@ -45,7 +45,7 @@ import random
|
||||
from collections import OrderedDict
|
||||
from xml.etree import ElementTree
|
||||
import json
|
||||
|
||||
from openpyxl.utils import get_column_letter
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
|
||||
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
|
||||
from reportlab.lib.colors import Color
|
||||
@ -127,6 +127,8 @@ class GenTable(object):
|
||||
filename="table", # filename, without extension
|
||||
xls_sheet_name="feuille",
|
||||
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_table_style=None,
|
||||
pdf_col_widths=None,
|
||||
@ -151,6 +153,8 @@ class GenTable(object):
|
||||
self.page_title = page_title
|
||||
self.pdf_link = pdf_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
|
||||
# HTML parameters:
|
||||
if not table_id: # random id
|
||||
@ -495,7 +499,8 @@ class GenTable(object):
|
||||
sheet = wb.create_sheet(sheet_name=self.xls_sheet_name)
|
||||
sheet.rows += self.xls_before_table
|
||||
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))
|
||||
for line in self.get_data_list(xls_mode=True):
|
||||
sheet.append_row(sheet.make_row(line, style_base))
|
||||
@ -505,6 +510,16 @@ class GenTable(object):
|
||||
if self.origin:
|
||||
sheet.append_blank_row() # empty line
|
||||
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:
|
||||
return sheet.generate()
|
||||
|
||||
|
@ -258,11 +258,16 @@ class ApoEtud(dict):
|
||||
self["nom"] = nom
|
||||
self["prenom"] = prenom
|
||||
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.etud = None # etud ScoDoc
|
||||
self.etud: Identite = None
|
||||
"etudiant ScoDoc associé"
|
||||
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.has_logged_no_decision = False
|
||||
self.export_res_etape = export_res_etape # VET, ...
|
||||
@ -276,7 +281,7 @@ class ApoEtud(dict):
|
||||
)
|
||||
|
||||
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):
|
||||
"""Cherche l'étudiant ScoDoc associé à cet étudiant Apogée.
|
||||
@ -284,6 +289,10 @@ class ApoEtud(dict):
|
||||
met .etud à None.
|
||||
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)
|
||||
if not etuds:
|
||||
# pas dans ScoDoc
|
||||
@ -291,13 +300,16 @@ class ApoEtud(dict):
|
||||
self.log.append("non inscrit dans ScoDoc")
|
||||
self.etat = ETUD_ORPHELIN
|
||||
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]
|
||||
# cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape:
|
||||
formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]}
|
||||
self.in_formsemestre_ids = formsemestre_ids.intersection(
|
||||
etape_formsemestre_ids
|
||||
)
|
||||
if not self.in_formsemestre_ids:
|
||||
in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
|
||||
if not in_formsemestre_ids:
|
||||
self.log.append(
|
||||
"connu dans ScoDoc, mais pas inscrit dans un semestre de cette étape"
|
||||
)
|
||||
@ -305,7 +317,7 @@ class ApoEtud(dict):
|
||||
else:
|
||||
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
|
||||
Set .new_cols
|
||||
"""
|
||||
@ -327,7 +339,7 @@ class ApoEtud(dict):
|
||||
cur_sem, autre_sem = self.etud_semestres_de_etape(apo_data)
|
||||
for sem in apo_data.sems_etape:
|
||||
el = self.search_elt_in_sem(code, sem, cur_sem, autre_sem)
|
||||
if el != None:
|
||||
if el is not None:
|
||||
sco_elts[code] = el
|
||||
break
|
||||
self.col_elts[code] = el
|
||||
@ -338,15 +350,15 @@ class ApoEtud(dict):
|
||||
self.new_cols[col_id] = sco_elts[code][
|
||||
apo_data.cols[col_id]["Type Rés."]
|
||||
]
|
||||
except KeyError:
|
||||
except KeyError as exc:
|
||||
log(
|
||||
"associate_sco: missing key, etud=%s\ncode='%s'\netape='%s'"
|
||||
% (self, code, apo_data.etape_apogee)
|
||||
f"associate_sco: missing key, etud={self}\ncode='{code}'\netape='{apo_data.etape_apogee}'"
|
||||
)
|
||||
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)?"""
|
||||
% code
|
||||
)
|
||||
f"""L'élément {code} 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)?"""
|
||||
) from exc
|
||||
# recopie les 4 premieres colonnes (nom, ..., naissance):
|
||||
for col_id in apo_data.col_ids[:4]:
|
||||
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])
|
||||
# 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
|
||||
ELP élément pédagogique: UE, module
|
||||
@ -820,10 +832,8 @@ class ApoData(object):
|
||||
elts[col["Code"]] = ApoElt([col])
|
||||
return elts # { code apo : ApoElt }
|
||||
|
||||
def apo_read_etuds(self, f):
|
||||
"""Lecture des etudiants (et resultats) du fichier CSV Apogée
|
||||
-> liste de dicts
|
||||
"""
|
||||
def apo_read_etuds(self, f) -> list[ApoEtud]:
|
||||
"""Lecture des etudiants (et resultats) du fichier CSV Apogée"""
|
||||
L = []
|
||||
while True:
|
||||
line = f.readline()
|
||||
@ -958,36 +968,38 @@ class ApoData(object):
|
||||
"""
|
||||
codes_by_sem = {}
|
||||
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()
|
||||
codes_by_sem[sem["formsemestre_id"]] = s
|
||||
for col_id in self.col_ids[4:]:
|
||||
code = self.cols[col_id]["Code"] # 'V1RT'
|
||||
# associé à l'étape, l'année ou les semestre:
|
||||
if (
|
||||
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(",")})
|
||||
):
|
||||
# associé à l'étape, l'année ou le semestre:
|
||||
if code in codes_semestre:
|
||||
s.add(code)
|
||||
continue
|
||||
# associé à une UE:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
for ue in nt.get_ues_stat_dict():
|
||||
if ue["code_apogee"]:
|
||||
codes = {x.strip() for x in ue["code_apogee"].split(",")}
|
||||
if code in codes:
|
||||
s.add(code)
|
||||
continue
|
||||
if code in codes_ues:
|
||||
s.add(code)
|
||||
continue
|
||||
# associé à un module:
|
||||
modimpls = nt.get_modimpls_dict()
|
||||
for modimpl in modimpls:
|
||||
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
|
||||
if code in codes_modules:
|
||||
s.add(code)
|
||||
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
|
||||
return codes_by_sem
|
||||
|
||||
|
@ -47,6 +47,7 @@
|
||||
qui est une description (humaine, format libre) de l'archive.
|
||||
|
||||
"""
|
||||
import chardet
|
||||
import datetime
|
||||
import glob
|
||||
import json
|
||||
@ -55,7 +56,7 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import chardet
|
||||
from typing import Union
|
||||
|
||||
import flask
|
||||
from flask import g, request
|
||||
@ -232,14 +233,17 @@ class BaseArchiver(object):
|
||||
os.mkdir(archive_id) # if exists, raises an OSError
|
||||
finally:
|
||||
scu.GSL.release()
|
||||
self.store(archive_id, "_description.txt", description.encode("utf-8"))
|
||||
self.store(archive_id, "_description.txt", description)
|
||||
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.
|
||||
Filename may be modified (sanitized): return used filename
|
||||
The file is created or replaced.
|
||||
data may be str or bytes
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
data = data.encode(scu.SCO_ENCODING)
|
||||
self.initialize()
|
||||
filename = scu.sanitize_filename(filename)
|
||||
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(),
|
||||
]
|
||||
)
|
||||
data = data.encode(scu.SCO_ENCODING)
|
||||
PVArchive.store(archive_id, "Tableau_moyennes.html", data)
|
||||
|
||||
# Bulletins en JSON
|
||||
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
|
||||
data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
|
||||
data_js = data_js.encode(scu.SCO_ENCODING)
|
||||
if data:
|
||||
PVArchive.store(archive_id, "Bulletins.json", data_js)
|
||||
# Decisions de jury, en XLS
|
||||
|
@ -58,7 +58,6 @@ from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_pvjury
|
||||
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
|
||||
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(
|
||||
formsemestre: FormSemestre,
|
||||
|
@ -92,7 +92,6 @@ def formsemestre_bulletinetud_published_dict(
|
||||
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
d = {"type": "classic", "version": "0"}
|
||||
|
||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
||||
published = True
|
||||
else:
|
||||
@ -134,6 +133,7 @@ def formsemestre_bulletinetud_published_dict(
|
||||
)
|
||||
d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients
|
||||
# Disponible pour publication ?
|
||||
d["publie"] = published
|
||||
if not published:
|
||||
return d # stop !
|
||||
|
||||
@ -364,8 +364,35 @@ def formsemestre_bulletinetud_published_dict(
|
||||
return d
|
||||
|
||||
|
||||
def dict_decision_jury(etudid, formsemestre_id, with_decisions=False):
|
||||
"dict avec decision pour bulletins json"
|
||||
def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict:
|
||||
"""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
|
||||
|
||||
d = {}
|
||||
|
@ -441,13 +441,13 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
t = {
|
||||
"titre": ue["acronyme"] + " " + (ue["titre"] or ""),
|
||||
"_titre_html": plusminus
|
||||
+ ue["acronyme"]
|
||||
+ (ue["acronyme"] or "")
|
||||
+ " "
|
||||
+ ue["titre"]
|
||||
+ (ue["titre"] or "")
|
||||
+ ' <span class="bul_ue_descr">'
|
||||
+ ue["ue_descr_txt"]
|
||||
+ (ue["ue_descr_txt"] or "")
|
||||
+ "</span>",
|
||||
"_titre_help": ue["ue_descr_txt"],
|
||||
"_titre_help": ue["ue_descr_txt"] or "",
|
||||
"_titre_colspan": 2,
|
||||
"module": ue_descr,
|
||||
"note": ue["moy_ue_txt"],
|
||||
|
@ -189,7 +189,7 @@ CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
|
||||
CODES_SEM_REO = {NAR: 1} # reorientation
|
||||
|
||||
CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée
|
||||
|
||||
CODES_RCUE_VALIDES = CODES_UE_VALIDES # Niveau RCUE validé
|
||||
# Pour le BUT:
|
||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||
CODES_RCUE = {ADM, AJ, CMP}
|
||||
@ -201,6 +201,7 @@ BUT_CODES_PASSAGE = {
|
||||
ADJ,
|
||||
PASD,
|
||||
PAS1NCI,
|
||||
ATJ,
|
||||
}
|
||||
|
||||
|
||||
|
@ -683,7 +683,7 @@ def module_edit(
|
||||
]
|
||||
# Choix des Apprentissages Critiques
|
||||
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)
|
||||
descr += (
|
||||
[
|
||||
|
@ -52,7 +52,6 @@ from app.scodoc.sco_exceptions import (
|
||||
)
|
||||
|
||||
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_edit_apc
|
||||
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",
|
||||
{"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
|
||||
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
|
||||
for mat in mats:
|
||||
@ -448,7 +451,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
<ul>"""
|
||||
for m in ue.modules:
|
||||
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>"""
|
||||
else:
|
||||
modules_div = ""
|
||||
|
@ -59,7 +59,7 @@ class COLORS(Enum):
|
||||
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,...
|
||||
# (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)
|
||||
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:
|
||||
value = ""
|
||||
elif value is True:
|
||||
|
@ -1206,7 +1206,7 @@ def formsemestre_tableau_modules(
|
||||
)
|
||||
H.append(
|
||||
'<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(
|
||||
|
@ -35,13 +35,17 @@ from app.models.etudiants import Identite
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
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.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
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.scolog import logdb
|
||||
from app.scodoc.sco_codes_parcours import *
|
||||
@ -111,7 +115,7 @@ def formsemestre_validation_etud_form(
|
||||
url_tableau = url_for(
|
||||
"notes.formsemestre_recapcomplet",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
modejury=1,
|
||||
mode_jury=1,
|
||||
formsemestre_id=formsemestre_id,
|
||||
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>
|
||||
"""
|
||||
)
|
||||
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"])
|
||||
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
|
||||
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
|
||||
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(
|
||||
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>"""
|
||||
)
|
||||
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):
|
||||
"""Suppression des decisions de jury pour un etudiant."""
|
||||
log("formsemestre_validation_suppress_etud( %s, %s)" % (formsemestre_id, etudid))
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||
try:
|
||||
# -- Validation du semestre et des UEs
|
||||
cursor.execute(
|
||||
"""delete from scolar_formsemestre_validation
|
||||
where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s""",
|
||||
args,
|
||||
)
|
||||
# -- Autorisations d'inscription
|
||||
cursor.execute(
|
||||
"""delete from scolar_autorisation_inscription
|
||||
where etudid = %(etudid)s and origin_formsemestre_id=%(formsemestre_id)s""",
|
||||
args,
|
||||
)
|
||||
cnx.commit()
|
||||
except:
|
||||
cnx.rollback()
|
||||
raise
|
||||
"""Suppression des décisions de jury pour un étudiant/formsemestre.
|
||||
Efface toutes les décisions enregistrées concernant ce formsemestre et cet étudiant:
|
||||
code semestre, UEs, autorisations d'inscription
|
||||
"""
|
||||
log(f"formsemestre_validation_suppress_etud( {formsemestre_id}, {etudid})")
|
||||
|
||||
# Validations jury classiques (semestres, UEs, autorisations)
|
||||
for v in ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=formsemestre_id
|
||||
):
|
||||
db.session.delete(v)
|
||||
for v in ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etudid, origin_formsemestre_id=formsemestre_id
|
||||
):
|
||||
db.session.delete(v)
|
||||
# Validations jury spécifiques BUT
|
||||
for v in ApcValidationRCUE.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=formsemestre_id
|
||||
):
|
||||
db.session.delete(v)
|
||||
for v in ApcValidationAnnee.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=formsemestre_id
|
||||
):
|
||||
db.session.delete(v)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
_invalidate_etud_formation_caches(
|
||||
|
@ -150,22 +150,22 @@ def import_users(users, force=""):
|
||||
* ok: import ok or aborted
|
||||
* messages: the list of messages
|
||||
* the # of users created
|
||||
"""
|
||||
""" Implémentation:
|
||||
|
||||
Implémentation:
|
||||
Pour chaque utilisateur à créer:
|
||||
* 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
|
||||
* créer utilisateur et mettre le mot de passe
|
||||
* 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
|
||||
"""
|
||||
|
||||
created = {} # uid créés
|
||||
if len(users) == 0:
|
||||
import_ok = False
|
||||
msg_list = ["Feuille vide ou illisible"]
|
||||
else:
|
||||
created = {} # liste de uid créés
|
||||
msg_list = []
|
||||
line = 1 # start from excel line #2
|
||||
import_ok = True
|
||||
@ -217,7 +217,7 @@ def import_users(users, force=""):
|
||||
else:
|
||||
import_ok = False
|
||||
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
|
||||
if import_ok:
|
||||
for u in created.values():
|
||||
@ -228,7 +228,7 @@ def import_users(users, force=""):
|
||||
db.session.commit()
|
||||
mail_password(u)
|
||||
else:
|
||||
created = [] # reset # of created users to 0
|
||||
created = {} # reset # of created users to 0
|
||||
return import_ok, msg_list, len(created)
|
||||
|
||||
|
||||
|
@ -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
|
||||
"""
|
||||
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 = {}
|
||||
path_dir = Path(scu.SCODOC_LOGOS_DIR)
|
||||
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):
|
||||
result = filename_parser.match(entry.name)
|
||||
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()
|
||||
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"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Logo(logoname='{self.logoname}', filename='{self.filename}')"
|
||||
|
||||
def _set_format(self, fmt):
|
||||
self.suffix = fmt
|
||||
self.filepath = self.basepath + "." + fmt
|
||||
|
@ -36,6 +36,7 @@ from flask_login import current_user
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.but import jury_but_view
|
||||
from app.models.etudiants import make_etud_args
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
@ -445,6 +446,10 @@ def ficheEtud(etudid=None):
|
||||
else:
|
||||
info["groupes_row"] = ""
|
||||
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>
|
||||
<div class="ficheEtud" id="ficheEtud"><table>
|
||||
<tr><td>
|
||||
@ -477,6 +482,8 @@ def ficheEtud(etudid=None):
|
||||
|
||||
%(inscriptions_mkup)s
|
||||
|
||||
%(but_infos_mkup)s
|
||||
|
||||
<div class="ficheadmission">
|
||||
%(adm_data)s
|
||||
|
||||
@ -513,7 +520,7 @@ def ficheEtud(etudid=None):
|
||||
"""
|
||||
header = html_sco_header.sco_header(
|
||||
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=[
|
||||
"libjs/jinplace-1.2.1.min.js",
|
||||
"js/ue_list.js",
|
||||
|
@ -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) )
|
||||
|
||||
|
||||
def SituationEtudParcours(etud, formsemestre_id):
|
||||
def SituationEtudParcours(etud: dict, formsemestre_id: int):
|
||||
"""renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)"""
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
# if formsemestre.formation.is_apc():
|
||||
# return SituationEtudParcoursBUT(etud, formsemestre_id, nt)
|
||||
|
||||
parcours = nt.parcours
|
||||
#
|
||||
if parcours.ECTS_ONLY:
|
||||
@ -121,10 +125,10 @@ def SituationEtudParcours(etud, formsemestre_id):
|
||||
return SituationEtudParcoursGeneric(etud, formsemestre_id, nt)
|
||||
|
||||
|
||||
class SituationEtudParcoursGeneric(object):
|
||||
class SituationEtudParcoursGeneric:
|
||||
"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()
|
||||
"""
|
||||
@ -132,7 +136,7 @@ class SituationEtudParcoursGeneric(object):
|
||||
self.etudid = etud["etudid"]
|
||||
self.formsemestre_id = formsemestre_id
|
||||
self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
self.nt = nt
|
||||
self.nt: NotesTableCompat = nt
|
||||
self.formation = self.nt.formsemestre.formation
|
||||
self.parcours = self.nt.parcours
|
||||
# Ce semestre est-il le dernier de la formation ? (e.g. semestre 4 du DUT)
|
||||
|
@ -52,7 +52,8 @@ from reportlab.platypus import Paragraph
|
||||
from reportlab.lib import styles
|
||||
|
||||
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.res_compat import NotesTableCompat
|
||||
@ -274,7 +275,10 @@ def dict_pvjury(
|
||||
_codes.add(ue["ue_code"])
|
||||
|
||||
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(
|
||||
etudid, formsemestre_id
|
||||
@ -501,7 +505,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
|
||||
from app.but import jury_but_recap
|
||||
|
||||
return jury_but_recap.formsemestre_saisie_jury_but(
|
||||
formsemestre, readonly=True, mode="recap"
|
||||
formsemestre, read_only=True, mode="recap"
|
||||
)
|
||||
# /XXX
|
||||
footer = html_sco_header.sco_footer()
|
||||
@ -795,7 +799,7 @@ def descrform_pvjury(sem):
|
||||
|
||||
def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
|
||||
"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:
|
||||
# tous les inscrits du semestre
|
||||
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,
|
||||
init_qtip=True,
|
||||
),
|
||||
"""<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>
|
||||
"""
|
||||
% formsemestre_id,
|
||||
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="{url_for(
|
||||
"notes.formsemestre_archive",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)}"
|
||||
>voir cette page</a></span></p>
|
||||
""",
|
||||
]
|
||||
F = html_sco_header.sco_footer()
|
||||
descr = descrform_lettres_individuelles()
|
||||
@ -839,7 +848,11 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
|
||||
return "\n".join(H) + "\n" + tf[1] + F
|
||||
elif tf[0] == -1:
|
||||
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:
|
||||
# submit
|
||||
@ -857,15 +870,17 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
|
||||
finally:
|
||||
PDFLOCK.release()
|
||||
if not pdfdoc:
|
||||
flash("Aucun étudiant n'a de décision de jury !")
|
||||
return flask.redirect(
|
||||
"formsemestre_status?formsemestre_id={}&head_message=Aucun%20%C3%A9tudiant%20n%27a%20de%20d%C3%A9cision%20de%20jury".format(
|
||||
formsemestre_id
|
||||
url_for(
|
||||
"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
|
||||
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)
|
||||
|
||||
|
||||
|
@ -45,13 +45,14 @@ from flask import g
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_bulletins_pdf
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_pdf
|
||||
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_parcours_dut import SituationEtudParcours
|
||||
from app.scodoc.sco_pdf import SU
|
||||
import sco_version
|
||||
|
||||
LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER
|
||||
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
|
||||
|
||||
|
||||
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"
|
||||
width = doc.pagesize[0] # - doc.pageTemplate.left_p - doc.pageTemplate.right_p
|
||||
foot = Frame(
|
||||
@ -78,24 +79,24 @@ def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True):
|
||||
showBoundary=0,
|
||||
)
|
||||
|
||||
LeftFootStyle = reportlab.lib.styles.ParagraphStyle({})
|
||||
LeftFootStyle.fontName = preferences["SCOLAR_FONT"]
|
||||
LeftFootStyle.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
|
||||
LeftFootStyle.leftIndent = 0
|
||||
LeftFootStyle.firstLineIndent = 0
|
||||
LeftFootStyle.alignment = TA_RIGHT
|
||||
RightFootStyle = reportlab.lib.styles.ParagraphStyle({})
|
||||
RightFootStyle.fontName = preferences["SCOLAR_FONT"]
|
||||
RightFootStyle.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
|
||||
RightFootStyle.alignment = TA_RIGHT
|
||||
left_foot_style = reportlab.lib.styles.ParagraphStyle({})
|
||||
left_foot_style.fontName = preferences["SCOLAR_FONT"]
|
||||
left_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
|
||||
left_foot_style.leftIndent = 0
|
||||
left_foot_style.firstLineIndent = 0
|
||||
left_foot_style.alignment = TA_RIGHT
|
||||
right_foot_style = reportlab.lib.styles.ParagraphStyle({})
|
||||
right_foot_style.fontName = preferences["SCOLAR_FONT"]
|
||||
right_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
|
||||
right_foot_style.alignment = TA_RIGHT
|
||||
|
||||
p = sco_pdf.makeParas(
|
||||
"""<para>%s</para><para>%s</para>"""
|
||||
% (preferences["INSTITUTION_NAME"], preferences["INSTITUTION_ADDRESS"]),
|
||||
LeftFootStyle,
|
||||
f"""<para>{preferences["INSTITUTION_NAME"]}</para><para>{
|
||||
preferences["INSTITUTION_ADDRESS"]}</para>""",
|
||||
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(
|
||||
[
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
||||
@ -123,7 +124,7 @@ def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True):
|
||||
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:
|
||||
return
|
||||
height = doc.pagesize[1]
|
||||
@ -260,7 +261,7 @@ class CourrierIndividuelTemplate(PageTemplate):
|
||||
|
||||
# ---- Header/Footer
|
||||
if self.with_header:
|
||||
pageHeader(
|
||||
page_header(
|
||||
canvas,
|
||||
doc,
|
||||
self.logo_header,
|
||||
@ -268,7 +269,7 @@ class CourrierIndividuelTemplate(PageTemplate):
|
||||
self.header_only_on_first_page,
|
||||
)
|
||||
if self.with_footer:
|
||||
pageFooter(
|
||||
page_footer(
|
||||
canvas,
|
||||
doc,
|
||||
self.logo_footer,
|
||||
@ -427,7 +428,7 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
|
||||
"""
|
||||
#
|
||||
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)
|
||||
objects = []
|
||||
style = reportlab.lib.styles.ParagraphStyle({})
|
||||
|
@ -57,7 +57,7 @@ from app.scodoc import sco_preferences
|
||||
|
||||
def formsemestre_recapcomplet(
|
||||
formsemestre_id=None,
|
||||
modejury=False,
|
||||
mode_jury=False,
|
||||
tabformat="html",
|
||||
sortcol=None,
|
||||
xml_with_decisions=False,
|
||||
@ -78,7 +78,7 @@ def formsemestre_recapcomplet(
|
||||
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)
|
||||
|
||||
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
|
||||
force_publishing: publie les xml et json même si bulletins non publiés
|
||||
selected_etudid: etudid sélectionné (pour scroller au bon endroit)
|
||||
@ -91,14 +91,14 @@ def formsemestre_recapcomplet(
|
||||
if tabformat not in supported_formats:
|
||||
raise ScoValueError(f"Format non supporté: {tabformat}")
|
||||
is_file = tabformat in file_formats
|
||||
modejury = int(modejury)
|
||||
mode_jury = int(mode_jury)
|
||||
xml_with_decisions = int(xml_with_decisions)
|
||||
force_publishing = int(force_publishing)
|
||||
|
||||
data = _do_formsemestre_recapcomplet(
|
||||
formsemestre_id,
|
||||
format=tabformat,
|
||||
modejury=modejury,
|
||||
mode_jury=mode_jury,
|
||||
sortcol=sortcol,
|
||||
xml_with_decisions=xml_with_decisions,
|
||||
force_publishing=force_publishing,
|
||||
@ -123,9 +123,9 @@ def formsemestre_recapcomplet(
|
||||
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
|
||||
"""
|
||||
)
|
||||
if modejury:
|
||||
if mode_jury:
|
||||
H.append(
|
||||
f'<input type="hidden" name="modejury" value="{modejury}"></input>'
|
||||
f'<input type="hidden" name="mode_jury" value="{mode_jury}"></input>'
|
||||
)
|
||||
H.append(
|
||||
'<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):
|
||||
H.append("<p>")
|
||||
if modejury:
|
||||
if mode_jury:
|
||||
H.append(
|
||||
f"""<a class="stdlink" href="{url_for('notes.formsemestre_validation_auto',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||
@ -172,7 +172,7 @@ def formsemestre_recapcomplet(
|
||||
else:
|
||||
H.append(
|
||||
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>"""
|
||||
)
|
||||
H.append("</p>")
|
||||
@ -196,7 +196,7 @@ def _do_formsemestre_recapcomplet(
|
||||
formsemestre_id=None,
|
||||
format="html", # html, xml, xls, xlsall, json
|
||||
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
|
||||
xml_with_decisions=False,
|
||||
force_publishing=True,
|
||||
@ -215,7 +215,7 @@ def _do_formsemestre_recapcomplet(
|
||||
formsemestre,
|
||||
res,
|
||||
include_evaluations=(format == "evals"),
|
||||
modejury=modejury,
|
||||
mode_jury=mode_jury,
|
||||
filename=filename,
|
||||
selected_etudid=selected_etudid,
|
||||
)
|
||||
@ -368,34 +368,34 @@ def gen_formsemestre_recapcomplet_html(
|
||||
formsemestre: FormSemestre,
|
||||
res: NotesTableCompat,
|
||||
include_evaluations=False,
|
||||
modejury=False,
|
||||
mode_jury=False,
|
||||
filename="",
|
||||
selected_etudid=None,
|
||||
):
|
||||
"""Construit table recap pour le BUT
|
||||
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
|
||||
data est une chaine, le <div>...</div> incluant le tableau.
|
||||
"""
|
||||
table_html = None
|
||||
if not (modejury or selected_etudid):
|
||||
if not (mode_jury or selected_etudid):
|
||||
if include_evaluations:
|
||||
table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id)
|
||||
else:
|
||||
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(
|
||||
formsemestre,
|
||||
res,
|
||||
include_evaluations,
|
||||
modejury,
|
||||
mode_jury,
|
||||
filename,
|
||||
selected_etudid=selected_etudid,
|
||||
)
|
||||
if not modejury:
|
||||
if not mode_jury:
|
||||
if include_evaluations:
|
||||
sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
|
||||
else:
|
||||
@ -408,13 +408,15 @@ def _gen_formsemestre_recapcomplet_html(
|
||||
formsemestre: FormSemestre,
|
||||
res: NotesTableCompat,
|
||||
include_evaluations=False,
|
||||
modejury=False,
|
||||
mode_jury=False,
|
||||
filename: str = "",
|
||||
selected_etudid=None,
|
||||
) -> str:
|
||||
"""Génère le html"""
|
||||
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:
|
||||
return (
|
||||
@ -423,7 +425,7 @@ def _gen_formsemestre_recapcomplet_html(
|
||||
H = [
|
||||
f"""<div class="table_recap"><table class="table_recap {
|
||||
'apc' if formsemestre.formation.is_apc() else 'classic'
|
||||
} {'jury' if modejury else ''}"
|
||||
} {'jury' if mode_jury else ''}"
|
||||
data-filename="{filename}">"""
|
||||
]
|
||||
# header
|
||||
|
@ -588,7 +588,7 @@ def purge_chars(s, 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"
|
||||
suppress accents and chars interpreted in XML
|
||||
Irreversible (not a quote)
|
||||
@ -596,8 +596,10 @@ def sanitize_string(s):
|
||||
For ids and some filenames
|
||||
"""
|
||||
# Table suppressing some chars:
|
||||
trans = str.maketrans("", "", "'`\"<>!&\\ ")
|
||||
return suppress_accents(s.translate(trans)).replace(" ", "_").replace("\t", "_")
|
||||
to_del = "'`\"<>!&\\ " if remove_spaces else "'`\"<>!&"
|
||||
trans = str.maketrans("", "", to_del)
|
||||
|
||||
return suppress_accents(s.translate(trans)).replace("\t", "_")
|
||||
|
||||
|
||||
_BAD_FILENAME_CHARS = str.maketrans("", "", ":/\\&[]*?'")
|
||||
@ -968,6 +970,8 @@ ICON_XLS = icontag("xlsicon_img", title="Version tableur")
|
||||
# HTML emojis
|
||||
EMO_WARNING = "⚠️" # warning /!\
|
||||
EMO_RED_TRIANGLE_DOWN = "🔻" # red triangle pointed down
|
||||
EMO_PREV_ARROW = "❮"
|
||||
EMO_NEXT_ARROW = "❯"
|
||||
|
||||
|
||||
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:
|
||||
klass = key + " " + klass
|
||||
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")
|
||||
if order:
|
||||
attrs += f' data-order="{order}"'
|
||||
|
@ -65,6 +65,19 @@
|
||||
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 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@ -73,9 +86,10 @@ div.but_settings {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
span.but_explanation {
|
||||
.but_explanation {
|
||||
color: blueviolet;
|
||||
font-style: italic;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
select:disabled {
|
||||
|
@ -189,10 +189,10 @@ section>div:nth-child(1){
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
#ects_tot {
|
||||
margin-left: 8px;
|
||||
#ects_tot, .decision, .decision_annee {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.enteteSemestre{
|
||||
color: black;
|
||||
|
@ -2980,7 +2980,8 @@ td.rcp_dec {
|
||||
;
|
||||
}
|
||||
|
||||
td.rcp_nonass {
|
||||
td.rcp_nonass,
|
||||
td.rcp_but {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@ -3770,6 +3771,7 @@ table.table_recap .rang {
|
||||
}
|
||||
|
||||
table.table_recap .col_ue,
|
||||
table.table_recap .col_ue_code,
|
||||
table.table_recap .col_moy_gen,
|
||||
table.table_recap .group {
|
||||
border-left: 1px solid blue;
|
||||
@ -3783,15 +3785,18 @@ table.table_recap.jury .col_ue {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -11,4 +11,52 @@ function change_menu_code(elt) {
|
||||
// TODO: comparer avec valeur enregistrée (à mettre en data-orig ?)
|
||||
// et colorer en fonction
|
||||
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 = "";
|
||||
}
|
||||
|
||||
}
|
||||
});
|
@ -83,7 +83,9 @@ class releveBUT extends HTMLElement {
|
||||
<div>
|
||||
<div class=infoSemestre></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>
|
||||
<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>
|
||||
@ -192,6 +194,20 @@ class releveBUT extends HTMLElement {
|
||||
/* Information sur le semestre */
|
||||
/*******************************/
|
||||
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(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription);
|
||||
@ -208,9 +224,26 @@ class releveBUT extends HTMLElement {
|
||||
<div class=abs>Non justifiées</div>
|
||||
<div>${data.semestre.absences?.injustifie ?? "-"}</div>
|
||||
<div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</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>
|
||||
`;
|
||||
</div>`;
|
||||
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 => {
|
||||
return `
|
||||
<div>
|
||||
@ -224,10 +257,17 @@ class releveBUT extends HTMLElement {
|
||||
}).join("")
|
||||
}*/
|
||||
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("#ects_tot").innerHTML = "ECTS : " + (data.semestre.ECTS?.acquis || "-") + " / " + (data.semestre.ECTS?.total || "-");
|
||||
}*/
|
||||
this.shadow.querySelector("#ects_tot").innerHTML = "ECTS : " + (data.semestre.ECTS?.acquis ?? "-") + " / " + (data.semestre.ECTS?.total ?? "-");
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
@ -254,13 +294,13 @@ class releveBUT extends HTMLElement {
|
||||
${ue}${(dataUE.titre) ? " - " + dataUE.titre : ""}
|
||||
</h3>
|
||||
<div>
|
||||
<div class=moyenne>Moyenne : ${dataUE.moyenne?.value || "-"}</div>
|
||||
<div class=moyenne>Moyenne : ${dataUE.moyenne?.value ?? "-"}</div>
|
||||
<div class=rang>Rang : ${dataUE.moyenne?.rang} / ${dataUE.moyenne?.total}</div>
|
||||
<div class=info>
|
||||
Bonus : ${dataUE.bonus || 0} -
|
||||
Malus : ${dataUE.malus || 0}
|
||||
<span class=ects> -
|
||||
ECTS : ${dataUE.ECTS?.acquis || "-"} / ${dataUE.ECTS?.total || "-"}
|
||||
ECTS : ${dataUE.ECTS?.acquis ?? "-"} / ${dataUE.ECTS?.total ?? "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
@ -11,6 +11,7 @@ function build_table(data) {
|
||||
let output = "";
|
||||
let sumsUE = {};
|
||||
let sumsRessources = {};
|
||||
let value;
|
||||
|
||||
data.forEach((cellule) => {
|
||||
output += `
|
||||
@ -31,13 +32,16 @@ function build_table(data) {
|
||||
--y:${cellule.y};
|
||||
--nbX:${cellule.nbX || 1};
|
||||
--nbY: ${cellule.nbY || 1};
|
||||
">
|
||||
${cellule.data}
|
||||
</div>`;
|
||||
|
||||
">${cellule.data}</div>`; // ne pas mettre d'espace car c'est utilisé par :not(:empty) après
|
||||
|
||||
if (cellule.style.includes("champs")) {
|
||||
sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + (parseFloat(cellule.data) || 0);
|
||||
sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + (parseFloat(cellule.data) || 0);
|
||||
if (cellule.editable == true && cellule.data) {
|
||||
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;
|
||||
--nbY:1;
|
||||
">
|
||||
${value}
|
||||
${value / 100}
|
||||
</div>`;
|
||||
})
|
||||
|
||||
@ -82,7 +86,7 @@ function showSums(sumsRessources, sumsUE) {
|
||||
--nbX:1;
|
||||
--nbY:1;
|
||||
">
|
||||
${value}
|
||||
${value / 100}
|
||||
</div>`;
|
||||
})
|
||||
|
||||
@ -186,16 +190,16 @@ function keyCell(event) {
|
||||
|
||||
function processSums() {
|
||||
let sum = 0;
|
||||
document.querySelectorAll(`[data-editable="true"][data-x="${this.dataset.x}"]`).forEach(e => {
|
||||
sum += parseFloat(e.innerText) || 0;
|
||||
document.querySelectorAll(`[data-editable="true"][data-x="${this.dataset.x}"]:not(:empty)`).forEach(e => {
|
||||
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;
|
||||
document.querySelectorAll(`[data-editable="true"][data-y="${this.dataset.y}"]`).forEach(e => {
|
||||
sum += parseFloat(e.innerText) || 0;
|
||||
document.querySelectorAll(`[data-editable="true"][data-y="${this.dataset.y}"]:not(:empty)`).forEach(e => {
|
||||
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;
|
||||
}
|
||||
|
||||
/******************************/
|
||||
|
@ -133,7 +133,7 @@ $(function () {
|
||||
}
|
||||
});
|
||||
}
|
||||
$('table.table_recap').DataTable(
|
||||
let table = $('table.table_recap').DataTable(
|
||||
{
|
||||
paging: false,
|
||||
searching: true,
|
||||
@ -146,6 +146,7 @@ $(function () {
|
||||
orderCellsTop: true, // cellules ligne 1 pour tri
|
||||
aaSorting: [], // Prevent initial sorting
|
||||
colReorder: true,
|
||||
stateSave: true, // enregistre état de la table (tris, ...)
|
||||
"columnDefs": [
|
||||
{
|
||||
// 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"
|
||||
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) {
|
||||
return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
|
||||
}
|
||||
@ -192,11 +193,22 @@ $(function () {
|
||||
if (formsemestre_id) {
|
||||
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,
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
$('table.table_recap tbody').on('click', 'tr', function () {
|
||||
if ($(this).hasClass('selected')) {
|
||||
@ -211,8 +223,8 @@ $(function () {
|
||||
$(function () {
|
||||
let row_selected = document.querySelector("#row_selected");
|
||||
if (row_selected) {
|
||||
row_selected.scrollIntoView();
|
||||
window.scrollBy(0, -50);
|
||||
/*row_selected.scrollIntoView();
|
||||
window.scrollBy(0, -50);*/
|
||||
row_selected.classList.add("selected");
|
||||
}
|
||||
});
|
||||
|
@ -1,9 +1,12 @@
|
||||
<div class="but_doc_codes">
|
||||
<p><em>Ci-dessous la signification de chaque code est expliquée,
|
||||
ainsi que la correspondance avec les codes préconisés par
|
||||
l'AMUE pour Apogée dans un document informel qui a circulé début
|
||||
2022 (les éventuelles erreurs n'engagent personne).
|
||||
</em></p>
|
||||
ainsi que la correspondance avec certains codes préconisés par
|
||||
l'AMUE et l'ADIUT pour Apogée.
|
||||
</em>
|
||||
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">
|
||||
<table>
|
||||
@ -63,6 +66,12 @@
|
||||
<td class="amue">ABAN</td>
|
||||
<td>ABANdon constaté (sans lettre de démission)</td>
|
||||
</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>
|
||||
<td>RAT</td>
|
||||
<td>{{codes["RAT"]}}</td>
|
||||
@ -124,6 +133,12 @@
|
||||
<td class="amue">AJ</td>
|
||||
<td>Attente pour problème de moyenne</td>
|
||||
</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>
|
||||
<td>RAT</td>
|
||||
<td>{{codes["RAT"]}}</td>
|
||||
@ -180,6 +195,12 @@
|
||||
<td class="amue">AJ</td>
|
||||
<td>Attente pour problème de moyenne</td>
|
||||
</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>
|
||||
<td>RAT</td>
|
||||
<td>{{codes["RAT"]}}</td>
|
||||
@ -212,4 +233,34 @@
|
||||
</tr>
|
||||
</table>
|
||||
</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 d’UE, la compensation est intégrale.
|
||||
Si une UE n’a pas été acquise en raison d’une 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 l’UE appartient.</li>
|
||||
<li>La poursuite d'études dans un semestre pair d’une 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 d’UE</li>
|
||||
<li>et une moyenne égale ou supérieure à 8 sur 20 à chaque regroupement cohérent d’UE.</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>
|
@ -31,7 +31,6 @@ Module notes: issu de ScoDoc7 / ZNotes.py
|
||||
Emmanuel Viennet, 2021
|
||||
"""
|
||||
|
||||
import html
|
||||
from operator import itemgetter
|
||||
import time
|
||||
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.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.res_but import ResultatsSemestreBUT
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
@ -56,23 +57,21 @@ from app.models.ues import UniteEns
|
||||
from app import api
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import ScolarNews
|
||||
from app.models import ScolarNews, but_validations
|
||||
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 (
|
||||
scodoc,
|
||||
scodoc7func,
|
||||
permission_required,
|
||||
permission_required_compat_scodoc7,
|
||||
admin_required,
|
||||
login_required,
|
||||
)
|
||||
|
||||
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 import log, send_scodoc_alarm
|
||||
|
||||
@ -297,7 +296,7 @@ def formsemestre_bulletinetud(
|
||||
format = format or "html"
|
||||
|
||||
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)
|
||||
if etudid:
|
||||
etud = models.Identite.query.get_or_404(etudid)
|
||||
@ -2144,6 +2143,16 @@ def formsemestre_validation_etud_form(
|
||||
):
|
||||
"Formulaire choix jury pour un étudiant"
|
||||
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(
|
||||
formsemestre_id,
|
||||
etudid=etudid,
|
||||
@ -2217,22 +2226,24 @@ def formsemestre_validation_etud_manu(
|
||||
|
||||
# --- Jurys BUT
|
||||
@bp.route(
|
||||
"/formsemestre_validation_but/<int:formsemestre_id>/<int:etudid>",
|
||||
"/formsemestre_validation_but/<int:formsemestre_id>/<etudid>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@scodoc
|
||||
@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"
|
||||
if not sco_permissions_check.can_validate_sem(formsemestre_id):
|
||||
return scu.confirm_dialog(
|
||||
message=f"<p>Opération non autorisée pour {current_user}</h2>",
|
||||
dest_url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
),
|
||||
)
|
||||
# la route ne donne pas le type d'etudid pour pouvoir construire des URLs
|
||||
# provisoires avec NEXT et PREV
|
||||
try:
|
||||
etudid = int(etudid)
|
||||
except:
|
||||
abort(404, "invalid etudid")
|
||||
read_only = not sco_permissions_check.can_validate_sem(formsemestre_id)
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Validation BUT",
|
||||
@ -2265,13 +2276,13 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
if len(deca.rcues_annee) == 0:
|
||||
raise ScoValueError("année incomplète: pas de jury BUT annuel possible")
|
||||
if request.method == "POST":
|
||||
deca.record_form(request.form)
|
||||
flash("codes enregistrés")
|
||||
if not read_only:
|
||||
deca.record_form(request.form)
|
||||
flash("codes enregistrés")
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_validation_but",
|
||||
@ -2280,13 +2291,7 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
|
||||
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 = ""
|
||||
if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau):
|
||||
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(
|
||||
f"""
|
||||
<div>
|
||||
<div class="titre_parcours">Jury BUT{deca.annee_but}
|
||||
- Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"}
|
||||
- {deca.annee_scolaire_str()}</div>
|
||||
<div class="nom_etud">{etud.nomprenom}</div>
|
||||
{warning}
|
||||
<div class="bull_head">
|
||||
<div>
|
||||
<div class="titre_parcours">Jury BUT{deca.annee_but}
|
||||
- Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"}
|
||||
- {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>
|
||||
|
||||
<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(
|
||||
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
|
||||
H.append(jury_but_view.show_etud(deca, read_only=read_only))
|
||||
|
||||
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} <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> {scu.EMO_NEXT_ARROW}
|
||||
"""
|
||||
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>
|
||||
f"""
|
||||
<div class="but_navigation">
|
||||
<div class="prev">
|
||||
{prev}
|
||||
</div>
|
||||
|
||||
<div class="but_buttons">
|
||||
<span><input type="submit" value="Enregistrer ces décisions"></span>
|
||||
<span><a href="{url_for(
|
||||
<div class="back_list">
|
||||
<a href="{url_for(
|
||||
"notes.formsemestre_saisie_jury", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id, selected_etudid=etud.id
|
||||
)}">retour à la liste</a></span>
|
||||
)}" class="stdlink">retour à la liste</a>
|
||||
</div>
|
||||
"""
|
||||
<div class="next">
|
||||
{next}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
H.append("</form>") # but_annee
|
||||
H.append("</form>")
|
||||
|
||||
H.append(
|
||||
render_template(
|
||||
@ -2399,48 +2386,6 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
|
||||
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(
|
||||
"/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(
|
||||
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):
|
||||
return scu.confirm_dialog(
|
||||
message="<p>Opération non autorisée pour %s</h2>" % current_user,
|
||||
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:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
sem = formsemestre.to_dict()
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
decision_jury = nt.get_etud_decision_sem(etudid)
|
||||
if decision_jury:
|
||||
existing = (
|
||||
"<p>Décision existante: %(code)s du %(event_date)s</p>" % decision_jury
|
||||
)
|
||||
d = sco_bulletins_json.dict_decision_jury(
|
||||
etudid, formsemestre_id, with_decisions=True
|
||||
)
|
||||
d.update(but_validations.dict_decision_jury(etud, formsemestre))
|
||||
|
||||
descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])]
|
||||
dec_annee = d.get("decision_annee")
|
||||
if dec_annee:
|
||||
descr_annee = dec_annee.get("code", "-")
|
||||
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(
|
||||
"""<h2>Confirmer la suppression des décisions du semestre %s (%s - %s) pour %s ?</h2>%s
|
||||
<p>Cette opération est irréversible.
|
||||
</p>
|
||||
"""
|
||||
% (
|
||||
sem["titre_num"],
|
||||
sem["date_debut"],
|
||||
sem["date_fin"],
|
||||
etud["nomprenom"],
|
||||
existing,
|
||||
),
|
||||
f"""<h2>Confirmer la suppression des décisions du semestre
|
||||
{formsemestre.titre_mois()} pour {etud.nomprenom}
|
||||
</h2>
|
||||
<p>Cette opération est irréversible.</p>
|
||||
<div>
|
||||
{existing}
|
||||
</div>
|
||||
""",
|
||||
OK="Supprimer",
|
||||
dest_url="",
|
||||
cancel_url="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s"
|
||||
% (formsemestre_id, etudid),
|
||||
cancel_url=next_url,
|
||||
parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
|
||||
)
|
||||
|
||||
sco_formsemestre_validation.formsemestre_validation_suppress_etud(
|
||||
formsemestre_id, etudid
|
||||
)
|
||||
return flask.redirect(
|
||||
scu.ScoURL()
|
||||
+ "/Notes/formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&head_message=Décision%%20supprimée"
|
||||
% (formsemestre_id, etudid)
|
||||
)
|
||||
flash("Décisions supprimées")
|
||||
return flask.redirect(next_url)
|
||||
|
||||
|
||||
# ------------- PV de JURY et archives
|
||||
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")
|
||||
@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
|
||||
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)
|
||||
if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0:
|
||||
return jury_but_recap.formsemestre_saisie_jury_but(
|
||||
formsemestre, readonly, selected_etudid=selected_etudid
|
||||
formsemestre, read_only, selected_etudid=selected_etudid
|
||||
)
|
||||
return redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_recapcomplet",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
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
|
||||
def formsemestre_jury_but_recap(formsemestre_id: int, selected_etudid: int = None):
|
||||
"""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)
|
||||
if not (formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0):
|
||||
raise ScoValueError(
|
||||
"formsemestre_jury_but_recap: réservé aux semestres pairs de 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"
|
||||
)
|
||||
|
||||
|
||||
|
2
sco_version.py
Normal file → Executable file
2
sco_version.py
Normal file → Executable file
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.3.5"
|
||||
SCOVERSION = "9.3.16"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -33,7 +33,7 @@ except NameError:
|
||||
|
||||
load_dotenv(os.path.join(BASEDIR, ".env"))
|
||||
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"
|
||||
SCODOC_USER = os.environ["SCODOC_USER"]
|
||||
SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"]
|
||||
@ -85,13 +85,13 @@ if r.status_code != 200:
|
||||
print(f"{len(r.json())} étudiants courants")
|
||||
|
||||
# Bulletin d'un BUT
|
||||
formsemestre_id = 1052 # A adapter
|
||||
etudid = 16400
|
||||
formsemestre_id = 1063 # A adapter
|
||||
etudid = 16450
|
||||
bul = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin")
|
||||
|
||||
# d'un DUT
|
||||
formsemestre_id = 1028 # A adapter
|
||||
etudid = 14721
|
||||
formsemestre_id = 1062 # A adapter
|
||||
etudid = 16309
|
||||
bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin")
|
||||
|
||||
|
||||
|
@ -21,14 +21,36 @@ import requests
|
||||
from app.api.formsemestres import formsemestre
|
||||
|
||||
from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
|
||||
|
||||
from tests.api.tools_test_api import (
|
||||
MODIMPL_FIELDS,
|
||||
verify_fields,
|
||||
MODIMPL_FIELDS,
|
||||
EVAL_FIELDS,
|
||||
SAISIE_NOTES_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
|
||||
ETUDID = 1
|
||||
@ -143,26 +165,318 @@ def test_formsemestre_apo(api_headers):
|
||||
assert isinstance(formsemestre["titre"], str)
|
||||
|
||||
### ERROR ###
|
||||
etape_apo_inexistante = "aoefiaozidaoẑidjnoaiznjd"
|
||||
r_error = requests.get(
|
||||
f"{API_URL}/formsemestre/apo/{etape_apo_inexistante}",
|
||||
headers=api_headers,
|
||||
verify=CHECK_CERTIFICATE,
|
||||
)
|
||||
assert r_error.status_code == 404
|
||||
# etape_apo_inexistante = "aoefiaozidaoẑidjnoaiznjd"
|
||||
# r_error = requests.get(
|
||||
# f"{API_URL}/formsemestre/apo/{etape_apo_inexistante}",
|
||||
# headers=api_headers,
|
||||
# verify=CHECK_CERTIFICATE,
|
||||
# )
|
||||
# assert r_error.status_code == 404
|
||||
|
||||
|
||||
def test_bulletins(api_headers):
|
||||
"""
|
||||
Route: /formsemestre/<int:formsemestre_id>/bulletins
|
||||
"""
|
||||
formsemestre_id = 1
|
||||
r = requests.get(
|
||||
API_URL + "/formsemestre/1/bulletins",
|
||||
f"{API_URL}/formsemestre/{formsemestre_id}/bulletins",
|
||||
headers=api_headers,
|
||||
verify=CHECK_CERTIFICATE,
|
||||
)
|
||||
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
|
||||
# def test_jury():
|
||||
|
@ -641,3 +641,79 @@ PARTITIONS_GROUPS_ETU_FIELDS = {
|
||||
"ne",
|
||||
"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",
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user