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

0
LICENSE Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

View File

@ -212,7 +212,9 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
formsemestres = query.order_by(FormSemestre.date_debut)
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
)

View File

@ -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"
return jsonify(
[formsemestre.to_dict(convert_parcours=True) for formsemestre in formsemestres]
)
else:
return jsonify(res)
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"])

View File

@ -14,6 +14,7 @@ from flask import url_for, g
from app.comp.res_but import ResultatsSemestreBUT
from app.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}
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"] = {

View File

@ -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 dune 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
View File

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

View File

@ -36,7 +36,7 @@ from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_saisie_jury_but(
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
View File

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

View File

@ -496,17 +496,26 @@ def compute_malus(
"""
ues_idx = [ue.id for ue in ues]
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)

View File

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

View File

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

View File

@ -43,7 +43,7 @@ from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu
from app.scodoc.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

View File

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

View File

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

View File

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

View File

@ -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
(e) le 7/06/1974
à Paris
"""
return f"""{self.nomprenom}{line_sep}{self.code_nip or ""}{line_sep}{self.e} le {self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{line_sep}à {self.lieu_naissance or ""}"""
def photo_html(self, title=None, size="small") -> str:
"""HTML img tag for the photo, either in small size (h90)
or original size (size=="orig")

View File

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

View File

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

View File

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

View File

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

View File

@ -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:
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:
if code in codes_modules:
s.add(code)
continue
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
return codes_by_sem

View File

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

View File

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

View File

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

View File

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

View File

@ -189,7 +189,7 @@ CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
CODES_SEM_REO = {NAR: 1} # reorientation
CODES_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,
}

View File

@ -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 += (
[

View File

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

View File

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

View File

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

View File

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

View File

@ -150,8 +150,8 @@ 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
@ -161,11 +161,11 @@ def import_users(users, force=""):
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)

View File

@ -121,7 +121,8 @@ def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
:return: le résultat de la recherche ou None si aucune image trouvée
"""
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

View File

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

View File

@ -109,10 +109,14 @@ class DecisionSem(object):
# log('%s: %s %s %s %s %s' % (self.codechoice,code_etat,new_code_prev,formsemestre_id_utilise_pour_compenser,devenir,assiduite) )
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)

View File

@ -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,6 +275,9 @@ def dict_pvjury(
_codes.add(ue["ue_code"])
d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq])
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(
@ -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)

View File

@ -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({})

View File

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

View File

@ -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 = "&#9888;&#65039;" # warning /!\
EMO_RED_TRIANGLE_DOWN = "&#128315;" # red triangle pointed down
EMO_PREV_ARROW = "&#10094;"
EMO_NEXT_ARROW = "&#10095;"
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}"'

View File

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

View File

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

View File

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

View File

@ -12,3 +12,51 @@ function change_menu_code(elt) {
// 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 = "";
}
}
});

View File

@ -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>`;
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>
<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>`
}
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&nbsp;:&nbsp;" + (data.semestre.ECTS?.acquis || "-") + "&nbsp;/&nbsp;" + (data.semestre.ECTS?.total || "-");
}*/
this.shadow.querySelector("#ects_tot").innerHTML = "ECTS&nbsp;:&nbsp;" + (data.semestre.ECTS?.acquis ?? "-") + "&nbsp;/&nbsp;" + (data.semestre.ECTS?.total ?? "-");
}
/*******************************/
@ -254,13 +294,13 @@ class releveBUT extends HTMLElement {
${ue}${(dataUE.titre) ? " - " + dataUE.titre : ""}
</h3>
<div>
<div class=moyenne>Moyenne&nbsp;:&nbsp;${dataUE.moyenne?.value || "-"}</div>
<div class=moyenne>Moyenne&nbsp;:&nbsp;${dataUE.moyenne?.value ?? "-"}</div>
<div class=rang>Rang&nbsp;:&nbsp;${dataUE.moyenne?.rang}&nbsp;/&nbsp;${dataUE.moyenne?.total}</div>
<div class=info>
Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;-
Malus&nbsp;:&nbsp;${dataUE.malus || 0}
<span class=ects>&nbsp;-
ECTS&nbsp;:&nbsp;${dataUE.ECTS?.acquis || "-"}&nbsp;/&nbsp;${dataUE.ECTS?.total || "-"}
ECTS&nbsp;:&nbsp;${dataUE.ECTS?.acquis ?? "-"}&nbsp;/&nbsp;${dataUE.ECTS?.total ?? "-"}
</span>
</div>
</div>`;

View File

@ -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;
}
/******************************/

View File

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

View File

@ -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 dUE, la compensation est intégrale.
Si une UE na pas été acquise en raison dune moyenne inférieure à 10,
cette UE sera acquise par compensation si et seulement si létudiant
a obtenu la moyenne au regroupement cohérent auquel lUE appartient.</li>
<li>La poursuite d'études dans un semestre pair dune même année est de droit
pour tout étudiant.
La poursuite détudes dans un semestre impair est possible
<em>si et seulement si</em> létudiant a obtenu :
<ul>
<li>la moyenne à plus de la moitié des regroupements cohérents dUE</li>
<li>et une moyenne égale ou supérieure à 8 sur 20 à chaque regroupement cohérent dUE.</li>
</ul>
</li>
<li>La poursuite d'études dans le semestre 5 nécessite de plus la validation de toutes les UE des
semestres 1 et 2 dans les conditions de validation des points 4.3 et 4.4, ou par décision de jury.</li>
</ul>
<b>Textes de référence:</b>
<ul>
<li><a href="https://www.enseignementsup-recherche.gouv.fr/fr/bo/21/Special4/ESRS2114777A.htm">Bulletin
officiel spécial n°4 du 17 juin 2021</a></li>
<li><a
href="https://cache.media.enseignementsup-recherche.gouv.fr//file/SPE4-MESRI-17-6-2021/19/4/SP4_ESR_17_6_2021_1413194.pdf">Version
pdf complète</a></li>
</ul>
</div>
</div>

View File

@ -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,11 +2276,11 @@ 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":
if not read_only:
deca.record_form(request.form)
flash("codes enregistrés")
return flask.redirect(
@ -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)}
@ -2295,77 +2300,34 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
warning += """<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>"""
H.append(
f"""
<div>
<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)">
@ -2376,15 +2338,40 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
</div>
<div class="but_buttons">
<span><input type="submit" value="Enregistrer ces décisions"></span>
<span><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>
<input type="submit" value="Enregistrer ces décisions">
</div>
"""
)
H.append("</form>") # but_annee
# --- Navigation
prev = f"""{scu.EMO_PREV_ARROW}&nbsp;<a href="{url_for(
"notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, etudid="PREV"
)}" class="stdlink"">précédent</a>
"""
next = f"""<a href="{url_for(
"notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, etudid="NEXT"
)}" class="stdlink"">suivant</a>&nbsp;{scu.EMO_NEXT_ARROW}
"""
H.append(
f"""
<div class="but_navigation">
<div class="prev">
{prev}
</div>
<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
)}" class="stdlink">retour à la liste</a>
</div>
<div class="next">
{next}
</div>
</div>
"""
)
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(),
)
if not dialog_confirmed:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
etud = Identite.query.get_or_404(etudid)
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
if formsemestre.formation.is_apc():
next_url = url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
)
else:
existing = ""
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>
next_url = url_for(
"notes.formsemestre_validation_etud_form",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
if not dialog_confirmed:
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:
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>
"""
% (
sem["titre_num"],
sem["date_debut"],
sem["date_fin"],
etud["nomprenom"],
existing,
),
return scu.confirm_dialog(
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"
)

0
bench.py Normal file → Executable file
View File

0
pylintrc Normal file → Executable file
View File

2
sco_version.py Normal file → Executable file
View File

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

View File

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

View File

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

View File

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