Merge branch 'master' into feuille_jury

This commit is contained in:
Jean-Marie Place 2023-06-21 07:41:58 +02:00
commit 5ba720baee
42 changed files with 1554 additions and 484 deletions

View File

@ -12,11 +12,20 @@ from flask_json import as_json
from flask_login import login_required
import app
from app.api import api_bp as bp, api_web_bp
from app import db, log
from app.api import api_bp as bp, api_web_bp, tools
from app.decorators import scodoc, permission_required
from app.scodoc.sco_exceptions import ScoException
from app.but import jury_but_results
from app.models import FormSemestre
from app.models import (
ApcValidationAnnee,
ApcValidationRCUE,
FormSemestre,
Identite,
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
)
from app.scodoc import sco_cache
from app.scodoc.sco_permissions import Permission
@ -36,3 +45,134 @@ def decisions_jury(formsemestre_id: int):
return rows
else:
raise ScoException("non implemente")
@bp.route(
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
methods=["POST"],
)
@api_web_bp.route(
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudInscrit)
@as_json
def validation_ue_delete(etudid: int, validation_id: int):
"Efface cette validation"
return _validation_ue_delete(etudid, validation_id)
@bp.route(
"/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete",
methods=["POST"],
)
@api_web_bp.route(
"/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete",
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudInscrit)
@as_json
def validation_formsemestre_delete(etudid: int, validation_id: int):
"Efface cette validation"
# c'est la même chose (formations classiques)
return _validation_ue_delete(etudid, validation_id)
def _validation_ue_delete(etudid: int, validation_id: int):
"Efface cette validation (semestres classiques ou UEs)"
etud = tools.get_etud(etudid)
if etud is None:
return "étudiant inconnu", 404
validation = ScolarFormSemestreValidation.query.filter_by(
id=validation_id, etudid=etudid
).first_or_404()
log(f"validation_ue_delete: etuid={etudid} {validation}")
db.session.delete(validation)
sco_cache.invalidate_formsemestre_etud(etud)
db.session.commit()
return "ok"
@bp.route(
"/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete",
methods=["POST"],
)
@api_web_bp.route(
"/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete",
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudInscrit)
@as_json
def autorisation_inscription_delete(etudid: int, validation_id: int):
"Efface cette validation"
etud = tools.get_etud(etudid)
if etud is None:
return "étudiant inconnu", 404
validation = ScolarAutorisationInscription.query.filter_by(
id=validation_id, etudid=etudid
).first_or_404()
log(f"autorisation_inscription_delete: etuid={etudid} {validation}")
db.session.delete(validation)
sco_cache.invalidate_formsemestre_etud(etud)
db.session.commit()
return "ok"
@bp.route(
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
methods=["POST"],
)
@api_web_bp.route(
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudInscrit)
@as_json
def validation_rcue_delete(etudid: int, validation_id: int):
"Efface cette validation"
etud = tools.get_etud(etudid)
if etud is None:
return "étudiant inconnu", 404
validation = ApcValidationRCUE.query.filter_by(
id=validation_id, etudid=etudid
).first_or_404()
log(f"validation_ue_delete: etuid={etudid} {validation}")
db.session.delete(validation)
sco_cache.invalidate_formsemestre_etud(etud)
db.session.commit()
return "ok"
@bp.route(
"/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete",
methods=["POST"],
)
@api_web_bp.route(
"/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete",
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudInscrit)
@as_json
def validation_annee_but_delete(etudid: int, validation_id: int):
"Efface cette validation"
etud = tools.get_etud(etudid)
if etud is None:
return "étudiant inconnu", 404
validation = ApcValidationAnnee.query.filter_by(
id=validation_id, etudid=etudid
).first_or_404()
log(f"validation_annee_but: etuid={etudid} {validation}")
db.session.delete(validation)
sco_cache.invalidate_formsemestre_etud(etud)
db.session.commit()
return "ok"

View File

@ -14,6 +14,7 @@ Classe raccordant avec ScoDoc 7:
"""
import collections
from operator import attrgetter
from typing import Union
from flask import g, url_for
@ -23,8 +24,6 @@ from app import log
from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat
from app.comp import res_sem
from app.models.but_refcomp import (
ApcAnneeParcours,
ApcCompetence,
@ -45,7 +44,7 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.codes_cursus import RED, UE_STANDARD
from app.scodoc.codes_cursus import code_ue_validant, RED, UE_STANDARD
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
@ -104,7 +103,7 @@ class EtudCursusBUT:
self.parcour: ApcParcours = self.inscriptions[-1].parcour
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
self.niveaux_by_annee = {}
"{ annee : liste des niveaux à valider }"
"{ annee:int : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
for annee in (1, 2, 3):
@ -118,21 +117,6 @@ class EtudCursusBUT:
self.niveaux.update(
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
)
# Probablement inutile:
# # Cherche les validations de jury enregistrées pour chaque niveau
# self.validations_by_niveau = collections.defaultdict(lambda: [])
# " { niveau_id : [ ApcValidationRCUE ] }"
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
# self.validations_by_niveau[validation_rcue.niveau().id].append(
# validation_rcue
# )
# self.validation_by_niveau = {
# niveau_id: sorted(
# validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
# )[0]
# for niveau_id, validations in self.validations_by_niveau.items()
# }
# "{ niveau_id : meilleure validation pour ce niveau }"
self.validation_par_competence_et_annee = {}
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
@ -146,7 +130,7 @@ class EtudCursusBUT:
# prend la "meilleure" validation
if (not previous_validation) or (
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
> sco_codes.BUT_CODES_ORDERED[previous_validation.code]
):
self.validation_par_competence_et_annee[niveau.competence.id][
niveau.annee
@ -206,6 +190,23 @@ class EtudCursusBUT:
)
return d
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
"""Cherche les validations de jury enregistrées pour chaque niveau
Résultat: { niveau_id : [ ApcValidationRCUE ] }
meilleure validation pour ce niveau
"""
validations_by_niveau = collections.defaultdict(lambda: [])
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=self.etud):
validations_by_niveau[validation_rcue.niveau().id].append(validation_rcue)
validation_by_niveau = {
niveau_id: sorted(
validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
)[0]
for niveau_id, validations in validations_by_niveau.items()
if validations
}
return validation_by_niveau
class FormSemestreCursusBUT:
"""L'état des étudiants d'un formsemestre dans leur cursus BUT
@ -358,6 +359,40 @@ class FormSemestreCursusBUT:
"cache { competence_id : competence }"
def etud_ues_de_but1_non_validees(
etud: Identite, formation: Formation, parcour: ApcParcours
) -> list[UniteEns]:
"""Vrai si cet étudiant a validé toutes ses UEs de S1 et S2, dans son parcours"""
# Les UEs avec décisions, dans les S1 ou S2 d'une formation de même code:
validations = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.filter(ScolarFormSemestreValidation.ue_id != None)
.join(UniteEns)
.filter(db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2))
.join(Formation)
.filter_by(formation_code=formation.formation_code)
)
codes_validations_by_ue = collections.defaultdict(list)
for v in validations:
codes_validations_by_ue[v.ue_id].append(v.code)
# Les UEs du parcours en S1 et S2:
ues = formation.query_ues_parcour(parcour).filter(
db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2)
)
# Liste triée des ues non validées
return sorted(
[
ue
for ue in ues
if any(
(not code_ue_validant(code) for code in codes_validations_by_ue[ue.id])
)
],
key=attrgetter("numero", "acronyme"),
)
def formsemestre_warning_apc_setup(
formsemestre: FormSemestre, res: ResultatsSemestreBUT
) -> str:

View File

@ -58,6 +58,7 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT
DecisionsProposeesRCUE appelera .set_compensable()
si on a la possibilité de la compenser dans le RCUE.
"""
from datetime import datetime
import html
from operator import attrgetter
import re
@ -68,15 +69,15 @@ from flask import flash, g, url_for
from app import db
from app import log
from app.but import cursus_but
from app.but.cursus_but import EtudCursusBUT
from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem
from app.models.but_refcomp import (
ApcAnneeParcours,
ApcCompetence,
ApcNiveau,
ApcParcours,
ApcParcoursNiveauCompetence,
)
from app.models import Scolog, ScolarAutorisationInscription
from app.models.but_validations import (
@ -86,12 +87,13 @@ from app.models.but_validations import (
)
from app.models.etudiants import Identite
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_cache
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.codes_cursus import (
code_rcue_validant,
BUT_CODES_ORDERED,
CODES_RCUE_VALIDES,
CODES_UE_CAPITALISANTS,
@ -275,7 +277,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_impair is not None:
self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
formsemestre_id=formsemestre_impair.id,
formation_id=self.formsemestre.formation_id,
ordre=self.annee_but,
).first()
else:
@ -360,15 +362,33 @@ class DecisionsProposeesAnnee(DecisionsProposees):
"Vrai si plus de la moitié des RCUE validables"
self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
"Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8"
# XXX TODO ajouter condition pour passage en S5
explanation = ""
# Cas particulier du passage en BUT 3: nécessité davoir validé toutes les UEs du BUT 1.
if self.passage_de_droit and self.annee_but == 2:
inscription = formsemestre.etuds_inscriptions.get(etud.id)
if inscription:
ues_but1_non_validees = cursus_but.etud_ues_de_but1_non_validees(
etud, formation, inscription.parcour
)
self.passage_de_droit = not ues_but1_non_validees
explanation += (
f"""UEs de BUT1 non validées: <b>{
', '.join(ue.acronyme for ue in ues_but1_non_validees)
}</b>. """
if ues_but1_non_validees
else ""
)
else:
# pas inscrit dans le semestre courant ???
self.passage_de_droit = False
# Enfin calcule les codes des UE:
# Enfin calcule les codes des UEs:
for dec_ue in self.decisions_ues.values():
dec_ue.compute_codes()
# Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
plural = self.nb_validables > 1
expl_rcues = f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
explanation += f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
"s" if plural else ""} sur {self.nb_competences}"""
if self.admis:
self.codes = [sco_codes.ADM] + self.codes
@ -387,7 +407,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.ABL,
sco_codes.EXCLU,
]
expl_rcues = ""
explanation = ""
elif self.passage_de_droit:
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante
@ -397,7 +417,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.PAS1NCI,
sco_codes.ADJ,
] + self.codes
expl_rcues += f" et {self.nb_rcues_under_8} < 8"
explanation += f" et {self.nb_rcues_under_8} < 8"
else:
self.codes = [
sco_codes.RED,
@ -406,14 +426,17 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.ADJ,
sco_codes.PASD, # voir #488 (discutable, conventions locales)
] + self.codes
expl_rcues += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
explanation += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
# Si l'un des semestres est extérieur, propose ADM
if (
self.formsemestre_impair and self.formsemestre_impair.modalite == "EXT"
) or (self.formsemestre_pair and self.formsemestre_pair.modalite == "EXT"):
self.codes.insert(0, sco_codes.ADM)
self.explanation = f"<div>{expl_rcues}</div>"
# Si validée par niveau supérieur:
if self.code_valide == sco_codes.ADSUP:
self.codes.insert(0, sco_codes.ADSUP)
self.explanation = f"<div>{explanation}</div>"
messages = self.descr_pb_coherence()
if messages:
self.explanation += (
@ -448,7 +471,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
scodoc_dept=g.scodoc_dept,
semestre_idx=formsemestre.semestre_id,
formation_id=formsemestre.formation.id)}">
{formsemestre.formation.to_html()} ({
{formsemestre.formation.html()} ({
formsemestre.formation.id})</a>
</li>
</ul>
@ -726,16 +749,18 @@ class DecisionsProposeesAnnee(DecisionsProposees):
dec_ue.record(code)
for dec_rcue, code in codes_rcues:
dec_rcue.record(code)
self.record(code_annee)
self.record(code_annee, mark_recorded=False)
self.record_autorisation_inscription(code_annee)
self.record_all()
self.recorded = True
db.session.commit()
def record(self, code: str, no_overwrite=False) -> bool:
def record(self, code: str, no_overwrite=False, mark_recorded: bool = True) -> bool:
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
Si no_overwrite, ne fait rien si un code est déjà enregistré.
Si l'étudiant est DEM ou DEF, ne fait rien.
Si mark_recorded est vrai, positionne self.recorded
"""
if self.inscription_etat != scu.INSCRIT:
return False
@ -755,6 +780,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
formation_id=self.formsemestre.formation_id,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
@ -766,7 +792,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: {code}",
)
self.recorded = True
if mark_recorded:
self.recorded = True
self.invalidate_formsemestre_cache()
return True
@ -873,7 +900,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
pour cette année: décisions d'UE, de RCUE, d'année,
et autorisations d'inscription émises.
Efface même si étudiant DEM ou DEF.
Si à cheval, n'efface que pour le semestre d'origine du deca.
Si à cheval ou only_one_sem, n'efface que les décisions UE et les
autorisations de passage du semestre d'origine du deca.
Dans tous les cas, efface les validations de l'année en cours.
(commite la session.)
"""
if only_one_sem or self.a_cheval:
@ -888,8 +918,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
else:
for dec_ue in self.decisions_ues.values():
dec_ue.erase()
for dec_rcue in self.decisions_rcue_by_niveau.values():
dec_rcue.erase()
if self.formsemestre_impair:
ScolarAutorisationInscription.delete_autorisation_etud(
self.etud.id, self.formsemestre_impair.id
@ -898,18 +927,27 @@ class DecisionsProposeesAnnee(DecisionsProposees):
ScolarAutorisationInscription.delete_autorisation_etud(
self.etud.id, self.formsemestre_pair.id
)
validations = ApcValidationAnnee.query.filter_by(
# Efface les RCUEs
for dec_rcue in self.decisions_rcue_by_niveau.values():
dec_rcue.erase()
# Efface les validations concernant l'année BUT
# de ce semestre
validations = (
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
formsemestre_id=self.formsemestre_impair.id,
ordre=self.annee_but,
)
for validation in validations:
db.session.delete(validation)
Scolog.logdb(
"jury_but",
etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: effacée",
)
.join(Formation)
.filter_by(formation_code=self.formsemestre.formation.formation_code)
)
for validation in validations:
db.session.delete(validation)
Scolog.logdb(
"jury_but",
etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: effacée",
)
# Efface éventuelles validations de semestre
# (en principe inutilisées en BUT)
@ -1035,6 +1073,9 @@ class DecisionsProposeesRCUE(DecisionsProposees):
):
super().__init__(etud=dec_prop_annee.etud)
self.deca = dec_prop_annee
self.referentiel_competence_id = (
self.deca.formsemestre.formation.referentiel_competence_id
)
self.rcue = rcue
if rcue is None: # RCUE non dispo, eg un seul semestre
self.codes = []
@ -1139,7 +1180,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
dec_ue.record(sco_codes.ADJR)
# Valide les niveaux inférieurs de la compétence (code ADSUP)
# TODO
if code in CODES_RCUE_VALIDES:
self.valide_niveau_inferieur()
if self.rcue.formsemestre_1 is not None:
sco_cache.invalidate_formsemestre(
@ -1177,6 +1219,191 @@ class DecisionsProposeesRCUE(DecisionsProposees):
return f"{niveau_titre}-{ordre}"
return ""
def valide_niveau_inferieur(self) -> None:
"""Appelé juste après la validation d'un RCUE.
*La validation des deux UE du niveau dune compétence emporte la validation de
lensemble des UEs du niveau inférieur de cette même compétence.*
"""
if not self.rcue or not self.rcue.ue_1 or not self.rcue.ue_1.niveau_competence:
return
competence: ApcCompetence = self.rcue.ue_1.niveau_competence.competence
ordre_inferieur = self.rcue.ue_1.niveau_competence.ordre - 1
if ordre_inferieur < 1:
return # pas de niveau inferieur
# --- Si le RCUE inférieur est déjà validé, ne fait rien
validations_rcue = (
ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
.join(ApcNiveau)
.filter_by(ordre=ordre_inferieur)
.join(ApcCompetence)
.filter_by(id=competence.id)
.all()
)
if [v for v in validations_rcue if code_rcue_validant(v.code)]:
return # déjà validé
# --- Validations des UEs
ues, ue1, ue2 = self._get_ues_inferieures(competence, ordre_inferieur)
# Pour chaque UE inférieure non validée, valide:
for ue in ues:
validations_ue = ScolarFormSemestreValidation.query.filter_by(
etudid=self.etud.id, ue_id=ue.id
).all()
if [
validation
for validation in validations_ue
if sco_codes.code_ue_validant(validation.code)
]:
continue # on a déjà une validation
# aucune validation validante
validation_ue = validations_ue[0] if validations_ue else None
if validation_ue:
# Modifie validation existante
validation_ue.code = sco_codes.ADSUP
validation_ue.event_date = datetime.now()
if validation_ue.formsemestre_id is not None:
sco_cache.invalidate_formsemestre(
formsemestre_id=validation_ue.formsemestre_id
)
log(f"updating {validation_ue}")
else:
# Ajoute une validation,
# pas de formsemestre ni de note car pas une capitalisation
validation_ue = ScolarFormSemestreValidation(
etudid=self.etud.id,
code=sco_codes.ADSUP,
ue_id=ue.id,
is_external=True, # pas rattachée à un formsemestre
)
log(f"recording {validation_ue}")
db.session.add(validation_ue)
# Valide le RCUE inférieur
if validations_rcue:
# Met à jour validation existante
validation_rcue = validations_rcue[0]
validation_rcue.code = sco_codes.ADSUP
validation_rcue.date = datetime.now()
db.session.add(validation_rcue)
db.session.commit()
log(f"updating {validation_rcue}")
if validation_rcue.formsemestre_id is not None:
sco_cache.invalidate_formsemestre(
formsemestre_id=validation_rcue.formsemestre_id
)
elif ue1 and ue2:
# Crée nouvelle validation
validation_rcue = ApcValidationRCUE(
etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP
)
db.session.add(validation_rcue)
db.session.commit()
log(f"recording {validation_rcue}")
self.valide_annee_inferieure()
def valide_annee_inferieure(self) -> None:
"""Si tous les RCUEs de l'année inférieure sont validés, la valide"""
# Indice de l'année inférieure:
annee_courante = self.rcue.ue_1.niveau_competence.annee # "BUT2"
if not re.match(r"^BUT\d$", annee_courante):
log("Warning: valide_annee_inferieure invalid annee_courante")
return
annee_inferieure = int(annee_courante[3]) - 1
if annee_inferieure < 1:
return
# Garde-fou: Année déjà validée ?
validations_annee: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=annee_inferieure,
formation_id=self.rcue.formsemestre_1.formation_id,
).all()
if len(validations_annee) > 1:
log(
f"warning: {len(validations_annee)} validations d'année\n{validations_annee}"
)
if [
validation_annee
for validation_annee in validations_annee
if sco_codes.code_annee_validant(validation_annee.code)
]:
return # déja valide
validation_annee = validations_annee[0] if validations_annee else None
# Liste des niveaux à valider:
# ici on sort l'artillerie lourde
cursus: EtudCursusBUT = EtudCursusBUT(
self.etud, self.rcue.formsemestre_1.formation
)
niveaux_a_valider = cursus.niveaux_by_annee[annee_inferieure]
# Pour chaque niveau, cherche validation RCUE
validations_by_niveau = cursus.load_validation_by_niveau()
ok = True
for niveau in niveaux_a_valider:
validation_niveau: ApcValidationRCUE = validations_by_niveau.get(niveau.id)
if not validation_niveau or not sco_codes.code_rcue_validant(
validation_niveau.code
):
ok = False
# Si tous OK, émet validation année
if validation_annee: # Modifie la validation antérieure (non validante)
validation_annee.code = sco_codes.ADSUP
validation_annee.date = datetime.now()
log(f"updating {validation_annee}")
else:
validation_annee = ApcValidationAnnee(
etudid=self.etud.id,
ordre=annee_inferieure,
code=sco_codes.ADSUP,
formation_id=self.rcue.formsemestre_1.formation_id,
# met cette validation sur l'année scolaire actuelle, pas la précédente (??)
annee_scolaire=self.rcue.formsemestre_1.annee_scolaire(),
)
log(f"recording {validation_annee}")
db.session.add(validation_annee)
db.session.commit()
def _get_ues_inferieures(
self, competence: ApcCompetence, ordre_inferieur: int
) -> tuple[list[UniteEns], UniteEns, UniteEns]:
"""Les UEs de cette formation associées au niveau de compétence inférieur ?
Note: on ne cherche que dans la formation courante, pas les UEs de
même code d'autres formations.
"""
formation: Formation = self.rcue.formsemestre_1.formation
ues: list[UniteEns] = (
UniteEns.query.filter_by(formation_id=formation.id)
.filter(UniteEns.semestre_idx != None)
.join(ApcNiveau)
.filter_by(ordre=ordre_inferieur)
.join(ApcCompetence)
.filter_by(id=competence.id)
.all()
)
log(f"valide_niveau_inferieur: {competence} UEs inférieures: {ues}")
if len(ues) != 2: # on n'a pas 2 UE associées au niveau inférieur !
flash(
"Impossible de valider le niveau de compétence inférieur: pas 2 UEs associées'",
"warning",
)
return [], None, None
ues_impaires = [ue for ue in ues if ue.semestre_idx % 2]
if len(ues_impaires) != 1:
flash(
"Impossible de valider le niveau de compétence inférieur: pas d'UE impaire associée"
)
return [], None, None
ue1 = ues_impaires[0]
ues_paires = [ue for ue in ues if not ue.semestre_idx % 2]
if len(ues_paires) != 1:
flash(
"Impossible de valider le niveau de compétence inférieur: pas d'UE paire associée"
)
return [], None, None
ue2 = ues_paires[0]
return ues, ue1, ue2
class DecisionsProposeesUE(DecisionsProposees):
"""Décisions de jury sur une UE du BUT
@ -1383,23 +1610,29 @@ class BUTCursusEtud: # WIP TODO
for competence in self.competences_du_parcours()
)
def est_diplome(self) -> bool:
"""Vrai si BUT déjà validé"""
# vrai si la troisième année est validée
# On cherche les validations de 3ieme annee (ordre=3) avec le même référentiel
# de formation que nous.
def est_annee_validee(self, ordre: int) -> bool:
"""Vrai si l'année BUT ordre est validée"""
# On cherche les validations d'annee avec le même
# code formation que nous.
return (
ApcValidationAnnee.query.filter_by(etudid=self.etud.id, ordre=3)
.join(FormSemestre, FormSemestre.id == ApcValidationAnnee.formsemestre_id)
.join(Formation, FormSemestre.formation_id == Formation.id)
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=ordre,
formation_id=self.formsemestre.formation_id,
)
.join(Formation)
.filter(
Formation.referentiel_competence_id
== self.formsemestre.formation.referentiel_competence_id
Formation.formation_code == self.formsemestre.formation.formation_code
)
.count()
> 0
)
def est_diplome(self) -> bool:
"""Vrai si BUT déjà validé"""
# vrai si la troisième année est validée
return self.est_annee_validee(3)
def competences_du_parcours(self) -> list[ApcCompetence]:
"""Construit liste des compétences du parcours, qui doivent être
validées pour obtenir le diplôme.

View File

@ -246,7 +246,7 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
scoplement = (
f"""<div class="scoplement">{
dec_rcue.validation.to_html()
dec_rcue.validation.html()
}</div>"""
if dec_rcue.validation
else ""

View File

@ -0,0 +1,66 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
Non spécifique au BUT.
"""
import flask
from flask import flash, render_template, url_for
from flask import g, request
from app import db
from app.models import (
ApcValidationAnnee,
ApcValidationRCUE,
FormSemestre,
Identite,
UniteEns,
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
)
from app.views import ScoData
def jury_delete_manual(etud: Identite):
"""Vue (réservée au chef de dept.)
présentant *toutes* les décisions de jury concernant cet étudiant
et permettant de les supprimer une à une.
"""
sem_vals = ScolarFormSemestreValidation.query.filter_by(
etudid=etud.id, ue_id=None
).order_by(ScolarFormSemestreValidation.event_date)
ue_vals = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.join(UniteEns)
.order_by(ScolarFormSemestreValidation.event_date, UniteEns.numero)
)
autorisations = ScolarAutorisationInscription.query.filter_by(
etudid=etud.id
).order_by(
ScolarAutorisationInscription.semestre_id, ScolarAutorisationInscription.date
)
rcue_vals = (
ApcValidationRCUE.query.filter_by(etudid=etud.id)
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
.order_by(UniteEns.semestre_idx, UniteEns.numero, ApcValidationRCUE.date)
)
annee_but_vals = ApcValidationAnnee.query.filter_by(etudid=etud.id).order_by(
ApcValidationAnnee.ordre, ApcValidationAnnee.date
)
return render_template(
"jury/jury_delete_manual.j2",
etud=etud,
sem_vals=sem_vals,
ue_vals=ue_vals,
autorisations=autorisations,
rcue_vals=rcue_vals,
annee_but_vals=annee_but_vals,
sco=ScoData(),
title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
)

View File

@ -10,8 +10,17 @@ import pandas as pd
import sqlalchemy as sa
from app import db
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
from app.comp.res_cache import ResultatsCache
from app.models import (
ApcValidationAnnee,
ApcValidationRCUE,
Formation,
FormSemestre,
Identite,
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
UniteEns,
)
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
@ -81,7 +90,7 @@ class ValidationsSemestre(ResultatsCache):
# UEs: { etudid : { ue_id : {"code":, "ects":, "event_date":} }}
decisions_jury_ues = {}
# Parcours les décisions d'UE:
# Parcoure les décisions d'UE:
for decision in (
decisions_jury_q.filter(db.text("ue_id is not NULL"))
.join(UniteEns)
@ -172,3 +181,80 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
with db.engine.begin() as connection:
df = pd.read_sql_query(query, connection, params=params, index_col="etudid")
return df
def erase_decisions_annee_formation(
etud: Identite, formation: Formation, annee: int, delete=False
) -> list:
"""Efface toutes les décisions de jury de l'étudiant dans les formations de même code
que celle donnée pour cette année de la formation:
UEs, RCUEs de l'année BUT, année BUT, passage vers l'année suivante.
Ne considère pas l'origine de la décision.
annee: entier, 1, 2, 3, ...
Si delete est faux, renvoie la liste des validations qu'il faudrait effacer, sans y toucher.
"""
sem1, sem2 = annee * 2 - 1, annee * 2
# UEs
validations = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.join(UniteEns)
.filter(db.or_(UniteEns.semestre_idx == sem1, UniteEns.semestre_idx == sem2))
.join(Formation)
.filter_by(formation_code=formation.formation_code)
.order_by(
UniteEns.acronyme, UniteEns.numero
) # acronyme d'abord car 2 semestres
.all()
)
# RCUEs (a priori inutile de matcher sur l'ue2_id)
validations += (
ApcValidationRCUE.query.filter_by(etudid=etud.id)
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
.filter_by(semestre_idx=sem1)
.join(Formation)
.filter_by(formation_code=formation.formation_code)
.order_by(UniteEns.acronyme, UniteEns.numero)
.all()
)
# Validation de semestres classiques
validations += (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id, ue_id=None)
.join(
FormSemestre,
FormSemestre.id == ScolarFormSemestreValidation.formsemestre_id,
)
.filter(
db.or_(FormSemestre.semestre_id == sem1, FormSemestre.semestre_id == sem2)
)
.join(Formation)
.filter_by(formation_code=formation.formation_code)
.all()
)
# Année BUT
validations += (
ApcValidationAnnee.query.filter_by(etudid=etud.id, ordre=annee)
.join(Formation)
.filter_by(formation_code=formation.formation_code)
.all()
)
# Autorisations vers les semestres suivants ceux de l'année:
validations += (
ScolarAutorisationInscription.query.filter_by(
etudid=etud.id, formation_code=formation.formation_code
)
.filter(
db.or_(
ScolarAutorisationInscription.semestre_id == sem1 + 1,
ScolarAutorisationInscription.semestre_id == sem2 + 1,
)
)
.all()
)
if delete:
for validation in validations:
db.session.delete(validation)
db.session.commit()
sco_cache.invalidate_formsemestre_etud(etud)
return []
return validations

View File

@ -10,17 +10,17 @@ import time
import numpy as np
import pandas as pd
from app import log
from app import db, log
from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_compat import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.models import Formation, FormSemestreInscription, ScoDocSiteConfig
from app.models.moduleimpls import ModuleImpl
from app.models.but_refcomp import ApcParcours, ApcNiveau
from app.models.ues import DispenseUE, UniteEns
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
from app.scodoc import sco_preferences
from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.codes_cursus import BUT_CODES_ORDERED, UE_SPORT
from app.scodoc.sco_utils import ModuleType
@ -44,7 +44,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
"""Parcours de chaque étudiant { etudid : parcour_id }"""
self.ues_ids_by_parcour: dict[set[int]] = {}
"""{ parcour_id : set }, ue_id de chaque parcours"""
self.validations_annee: dict[int, ApcValidationAnnee] = {}
"""chargé par get_validations_annee: jury annuel BUT"""
if not self.load_cached():
t0 = time.time()
self.compute()
@ -307,9 +308,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
return ues_ids
def etud_has_decision(self, etudid) -> bool:
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
"""True s'il y a une décision (quelconque) de jury
émanant de ce formsemestre pour cet étudiant.
prend aussi en compte les autorisations de passage.
Sous-classée en BUT pour les RCUEs et années.
Ici sous-classée (BUT) pour les RCUEs et années.
"""
return bool(
super().etud_has_decision(etudid)
@ -320,3 +322,42 @@ class ResultatsSemestreBUT(NotesTableCompat):
formsemestre_id=self.formsemestre.id, etudid=etudid
).count()
)
def get_validations_annee(self) -> dict[int, ApcValidationAnnee]:
"""Les validations des étudiants de ce semestre
pour l'année BUT d'une formation compatible avec celle de ce semestre.
Attention:
1) la validation ne provient pas nécessairement de ce semestre
(redoublants, pair/impair, extérieurs).
2) l'étudiant a pu démissionner ou défaillir.
3) S'il y a plusieurs validations pour le même étudiant, prend la "meilleure".
Mémorise le résultat (dans l'instance, pas en cache: TODO voir au profiler)
"""
if self.validations_annee:
return self.validations_annee
annee_but = (self.formsemestre.semestre_id + 1) // 2
validations = (
ApcValidationAnnee.query.filter_by(ordre=annee_but)
.join(Formation)
.filter_by(formation_code=self.formsemestre.formation.formation_code)
.join(
FormSemestreInscription,
db.and_(
FormSemestreInscription.etudid == ApcValidationAnnee.etudid,
FormSemestreInscription.formsemestre_id == self.formsemestre.id,
),
)
)
validation_by_etud = {}
for validation in validations:
if validation.etudid in validation_by_etud:
# keep the "best"
if BUT_CODES_ORDERED.get(validation.code, 0) > BUT_CODES_ORDERED.get(
validation_by_etud[validation.etudid].code, 0
):
validation_by_etud[validation.etudid] = validation
else:
validation_by_etud[validation.etudid] = validation
self.validations_annee = validation_by_etud
return self.validations_annee

View File

@ -66,7 +66,7 @@ class ApcValidationRCUE(db.Model):
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
def to_html(self) -> str:
def html(self) -> str:
"description en HTML"
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
<b>{self.code}</b>
@ -320,7 +320,12 @@ class ApcValidationAnnee(db.Model):
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
)
"le semestre IMPAIR (le 1er) de l'année"
annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
formation_id = db.Column(
db.Integer,
db.ForeignKey("notes_formations.id"),
nullable=False,
)
annee_scolaire = db.Column(db.Integer, nullable=False) # eg 2021
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
@ -343,20 +348,31 @@ class ApcValidationAnnee(db.Model):
"ordre": self.ordre,
}
def html(self) -> str:
"Affichage html"
return f"""Validation <b>année BUT{self.ordre}</b> émise par
{self.formsemestre.html_link_status() if self.formsemestre else "-"}
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
"""
def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
"""
Un dict avec les décisions de jury BUT enregistrées:
- decision_rcue : list[dict]
- decision_annee : dict
- decision_annee : dict (décision issue de ce semestre seulement (à confirmer ?))
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
validations_rcues = (
ApcValidationRCUE.query.filter_by(
etudid=etud.id, formsemestre_id=formsemestre.id
)
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
.order_by(UniteEns.numero, UniteEns.acronyme)
)
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
titres_rcues = []
@ -383,8 +399,7 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
etudid=etud.id,
annee_scolaire=formsemestre.annee_scolaire(),
)
.join(ApcValidationAnnee.formsemestre)
.join(FormSemestre.formation)
.join(Formation)
.filter(Formation.formation_code == formsemestre.formation.formation_code)
.first()
)

View File

@ -78,6 +78,12 @@ class Identite(db.Model):
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
)
def html_link_fiche(self) -> str:
"lien vers la fiche"
return f"""<a class="stdlink" href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id)
}">{self.nomprenom}</a>"""
@classmethod
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
"""Étudiant à partir de l'etudid ou du code_nip, soit

View File

@ -60,7 +60,7 @@ class Formation(db.Model):
return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
def to_html(self) -> str:
def html(self) -> str:
"titre complet pour affichage"
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""

View File

@ -16,7 +16,7 @@ from operator import attrgetter
from flask_login import current_user
from flask import flash, g
from flask import flash, g, url_for
from sqlalchemy.sql import text
import app.scodoc.sco_utils as scu
@ -163,6 +163,14 @@ class FormSemestre(db.Model):
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
def html_link_status(self) -> str:
"html link to status page"
return f"""<a class="stdlink" href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
formsemestre_id=self.id,)
}">{self.titre_mois()}</a>
"""
@classmethod
def get_formsemestre(cls, formsemestre_id: int) -> "FormSemestre":
""" "FormSemestre ou 404, cherche uniquement dans le département courant"""
@ -859,7 +867,7 @@ class FormSemestre(db.Model):
.order_by(UniteEns.numero)
.all()
)
vals_annee = (
vals_annee = ( # issues de ce formsemestre seulement
ApcValidationAnnee.query.filter_by(
etudid=etudid,
annee_scolaire=self.annee_scolaire(),

View File

@ -242,7 +242,7 @@ class Module(db.Model):
"les coefs d'UE, trié par numéro et acronyme d'UE"
# je n'ai pas su mettre un order_by sur le backref sans avoir
# à redéfinir les relationships...
return sorted(self.ue_coefs, key=attrgetter("numero", "acronyme"))
return sorted(self.ue_coefs, key=lambda uc: (uc.ue.numero, uc.ue.acronyme))
def ue_coefs_list(
self, include_zeros=True, ues: list["UniteEns"] = None

View File

@ -11,7 +11,7 @@ from app.models.events import Scolog
class ScolarFormSemestreValidation(db.Model):
"""Décisions de jury"""
"""Décisions de jury (sur semestre ou UEs)"""
__tablename__ = "scolar_formsemestre_validation"
# Assure unicité de la décision:
@ -59,13 +59,16 @@ class ScolarFormSemestreValidation(db.Model):
)
def __repr__(self):
return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
return f"""{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={
self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"""
def __str__(self):
if self.ue_id:
# Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue !
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id}: {self.code}"""
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {self.event_date.strftime("%d/%m/%Y")}"""
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
}: {self.code}"""
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
self.event_date.strftime("%d/%m/%Y")}"""
def to_dict(self) -> dict:
"as a dict"
@ -73,6 +76,28 @@ class ScolarFormSemestreValidation(db.Model):
d.pop("_sa_instance_state", None)
return d
def html(self, detail=False) -> str:
"Affichage html"
if self.ue_id is not None:
return f"""Validation de l'UE <b>{self.ue.acronyme}</b>
{('parcours <span class="parcours">'
+ ", ".join([p.code for p in self.ue.parcours]))
+ "</span>"
if self.ue.parcours else ""}
de {self.ue.formation.acronyme}
{("émise par " + self.formsemestre.html_link_status())
if self.formsemestre else ""}
: <b>{self.code}</b>
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
else:
return f"""Validation du semestre S{
self.formsemestre.semestre_id if self.formsemestre else "?"}
{self.formsemestre.html_link_status() if self.formsemestre else ""}
: <b>{self.code}</b>
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
class ScolarAutorisationInscription(db.Model):
"""Autorisation d'inscription dans un semestre"""
@ -93,6 +118,7 @@ class ScolarAutorisationInscription(db.Model):
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
origin_formsemestre = db.relationship("FormSemestre", lazy="select", uselist=False)
def __repr__(self) -> str:
return f"""{self.__class__.__name__}(id={self.id}, etudid={
@ -104,6 +130,15 @@ class ScolarAutorisationInscription(db.Model):
d.pop("_sa_instance_state", None)
return d
def html(self) -> str:
"Affichage html"
return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par
{self.origin_formsemestre.html_link_status()
if self.origin_formsemestre
else "-"}
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
"""
@classmethod
def autorise_etud(
cls,

View File

@ -122,6 +122,7 @@ ABAN = "ABAN"
ABL = "ABL"
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
ADSUP = "ADSUP" # BUT: UE ou RCUE validé par niveau supérieur
ADJ = "ADJ" # admis par le jury
ADJR = "ADJR" # UE admise car son RCUE est ADJ
ATT = "ATT" #
@ -162,6 +163,7 @@ CODES_EXPL = {
ADJ: "Validé par le Jury",
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
ADM: "Validé",
ADSUP: "UE ou RCUE validé car le niveau supérieur est validé",
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
@ -195,17 +197,18 @@ CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
CODES_SEM_REO = {NAR} # reorientation
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
"UE validée"
CODES_UE_CAPITALISANTS = {ADM}
"UE capitalisée"
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
"Niveau RCUE validé"
# Pour le BUT:
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD} # PASD pour enregistrement auto
CODES_ANNEE_BUT_VALIDES = {ADM, ADSUP}
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
@ -229,6 +232,7 @@ BUT_CODES_ORDERED = {
PASD: 50,
PAS1NCI: 60,
ADJR: 90,
ADSUP: 90,
ADJ: 100,
ADM: 100,
}
@ -249,6 +253,16 @@ def code_ue_validant(code: str) -> bool:
return code in CODES_UE_VALIDES
def code_rcue_validant(code: str) -> bool:
"Vrai si ce code d'RCUE est validant"
return code in CODES_RCUE_VALIDES
def code_annee_validant(code: str) -> bool:
"Vrai si code d'année BUT validant"
return code in CODES_ANNEE_BUT_VALIDES
DEVENIR_EXPL = {
NEXT: "Passage au semestre suivant",
REDOANNEE: "Redoublement année",

View File

@ -473,7 +473,10 @@ class ApoEtud(dict):
)
def _but_load_validation_annuelle(self):
"charge la validation de jury BUT annuelle"
"""charge la validation de jury BUT annuelle.
Ici impose qu'elle soit issue d'un semestre de l'année en cours
(pas forcément nécessaire, voir selon les retours des équipes ?)
"""
# le semestre impair de l'année scolaire
if self.cur_res.formsemestre.semestre_id % 2:
formsemestre = self.cur_res.formsemestre
@ -490,7 +493,9 @@ class ApoEtud(dict):
return
self.validation_annee_but: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id, etudid=self.etud["etudid"]
formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"],
formation_id=self.cur_sem["formation_id"],
).first()
)
self.is_nar = (

View File

@ -344,7 +344,7 @@ def do_formsemestre_archive(
if data:
PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data)
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
table_html, _ = gen_formsemestre_recapcomplet_html_table(
table_html, _, _ = gen_formsemestre_recapcomplet_html_table(
formsemestre, res, include_evaluations=True
)
if table_html:

View File

@ -315,6 +315,19 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
def invalidate_formsemestre_etud(etud: "Identite"):
"""Invalide tous les formsemestres auxquels l'étudiant est inscrit"""
from app.models import FormSemestre, FormSemestreInscription
inscriptions = (
FormSemestreInscription.query.filter_by(etudid=etud.id)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
)
for inscription in inscriptions:
invalidate_formsemestre(inscription.formsemestre_id)
class DeferredSemCacheManager:
"""Contexte pour effectuer des opérations indépendantes dans la
même requete qui invalident le cache. Par exemple, quand on inscrit

View File

@ -757,7 +757,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
],
page_title=f"Programme {formation.acronyme} v{formation.version}",
),
f"""<h2>{formation.to_html()} {lockicon}
f"""<h2>{formation.html()} {lockicon}
</h2>
""",
]
@ -1010,12 +1010,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<p><ul>"""
)
for formsemestre in formsemestres:
H.append(
f"""<li><a class="stdlink" href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id
)}">{formsemestre.titre_mois()}</a>"""
)
H.append(f"""<li>{formsemestre.html_link_status()}""")
if not formsemestre.etat:
H.append(" [verrouillé]")
else:

View File

@ -66,6 +66,7 @@ from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_pv_dict
# ------------------------------------------------------------------------------------
def formsemestre_validation_etud_form(
formsemestre_id=None, # required
@ -1063,8 +1064,6 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"""Form. saisie UE validée hors ScoDoc
(pour étudiants arrivant avec un UE antérieurement validée).
"""
from app.scodoc import sco_formations
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
@ -1087,8 +1086,8 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
<em>dans un semestre hors ScoDoc</em>.</p>
<p><b>Les UE validées dans ScoDoc sont déjà
automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant
suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré <b>sans
ScoDoc</b> et qui <b>redouble</b> ce semestre
suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré
<b>sans ScoDoc</b> et qui <b>redouble</b> ce semestre
(<em>ne pas utiliser pour les semestres précédents !</em>).
</p>
<p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et

View File

@ -312,7 +312,13 @@ def ficheEtud(etudid=None):
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
url_for("notes.formsemestre_inscription_with_modules_form",
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">inscrire à un autre semestre</a></span>"""
}">inscrire à un autre semestre</a></span>
<span class="link_bul_pdf"><a class="stdlink" href="{
url_for("notes.jury_delete_manual",
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">éditer toutes décisions de jury</a></span>
"""
else:
info["link_inscrire_ailleurs"] = ""
else:

View File

@ -27,6 +27,7 @@
"""Tableau récapitulatif des notes d'un semestre
"""
import collections
import datetime
import time
from xml.etree import ElementTree
@ -109,7 +110,7 @@ def formsemestre_recapcomplet(
force_publishing=force_publishing,
)
table_html, table = _formsemestre_recapcomplet_to_html(
table_html, table, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
formsemestre,
filename=filename,
mode_jury=mode_jury,
@ -142,7 +143,7 @@ def formsemestre_recapcomplet(
H.append(
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
)
for (fmt, label) in (
for fmt, label in (
("html", "Tableau"),
("evals", "Avec toutes les évaluations"),
("xlsx", "Excel (non formaté)"),
@ -186,7 +187,7 @@ def formsemestre_recapcomplet(
</li>
<li><a class="stdlink" href="{url_for('notes.formsemestre_jury_but_erase',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1)
}">Effacer <em>toutes</em> les décisions de jury (BUT) du semestre</a>
}">Effacer <em>toutes</em> les décisions de jury BUT issues de ce semestre</a>
</li>
"""
)
@ -215,33 +216,37 @@ def formsemestre_recapcomplet(
"""
)
if mode_jury and table and sum(table.freq_codes_annuels.values()) > 0:
if mode_jury and freq_codes_annuels and sum(freq_codes_annuels.values()) > 0:
nb_etud_avec_decision_annuelle = (
sum(freq_codes_annuels.values()) - freq_codes_annuels["total"]
)
H.append(
f"""
<div class="jury_stats">
<div>Nb d'étudiants avec décision annuelle:
{sum(table.freq_codes_annuels.values())} / {len(table)}
<div><b>Nb d'étudiants avec décision annuelle:</b>
{nb_etud_avec_decision_annuelle} / {freq_codes_annuels["total"]}
</div>
<div><b>Codes annuels octroyés:</b></div>
<table class="jury_stats_codes">
"""
)
for code in sorted(table.freq_codes_annuels.keys()):
if nb_etud_avec_decision_annuelle > 0:
H.append(
f"""<tr>
<td>{code}</td>
<td style="text-align:right">{table.freq_codes_annuels[code]}</td>
<td style="text-align:right">{
(100*table.freq_codes_annuels[code] / len(table)):2.1f}%
</td>
</tr>"""
"""<div><b>Codes annuels octroyés:</b></div>
<table class="jury_stats_codes">
"""
)
H.append(
"""
</table>
</div>
"""
)
for code in sorted(freq_codes_annuels.keys()):
if code != "total":
H.append(
f"""<tr>
<td>{code}</td>
<td style="text-align:right">{freq_codes_annuels[code]}</td>
<td style="text-align:right">{
(100*freq_codes_annuels[code] / freq_codes_annuels["total"]):2.1f}%
</td>
</tr>"""
)
H.append("""</table>""")
H.append("""</div>""")
# Légende
H.append(
"""
@ -251,6 +256,7 @@ def formsemestre_recapcomplet(
<div><tt>~</tt></div><div>valeur manquante</div>
<div><tt>=</tt></div><div>UE dispensée</div>
<div><tt>nan</tt></div><div>valeur non disponible</div>
<div>📍</div><div>code jury non enregistré</div>
</div>
</div>
"""
@ -271,12 +277,12 @@ def _formsemestre_recapcomplet_to_html(
filename: str = "",
mode_jury=False, # saisie décisions jury
selected_etudid=None,
) -> tuple[str, TableRecap]:
) -> tuple[str, TableRecap, collections.Counter]:
"""Le tableau recap en html"""
if tabformat not in ("html", "evals"):
raise ScoValueError("invalid table format")
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
table_html, table = gen_formsemestre_recapcomplet_html_table(
table_html, table, freq_codes_annuels = gen_formsemestre_recapcomplet_html_table(
formsemestre,
res,
include_evaluations=(tabformat == "evals"),
@ -284,7 +290,7 @@ def _formsemestre_recapcomplet_to_html(
filename=filename,
selected_etudid=selected_etudid,
)
return table_html, table
return table_html, table, freq_codes_annuels
def _formsemestre_recapcomplet_to_file(
@ -446,9 +452,9 @@ def gen_formsemestre_recapcomplet_html_table(
mode_jury=False,
filename="",
selected_etudid=None,
) -> tuple[str, TableRecap]:
) -> tuple[str, TableRecap, collections.Counter]:
"""Construit table recap pour le BUT
Cache le résultat pour le semestre (sauf en mode jury).
Cache le résultat pour le semestre.
Note: on cache le HTML et non l'objet Table.
Si mode_jury, occultera colonnes modules (en js)
@ -460,6 +466,7 @@ def gen_formsemestre_recapcomplet_html_table(
"""
table = None
table_html = None
table_html_cached = None
cache_class = {
(True, True): sco_cache.TableJuryWithEvalsCache,
(True, False): sco_cache.TableJuryCache,
@ -467,8 +474,8 @@ def gen_formsemestre_recapcomplet_html_table(
(False, False): sco_cache.TableRecapCache,
}[(bool(mode_jury), bool(include_evaluations))]
if not selected_etudid:
table_html = cache_class.get(formsemestre.id)
if table_html is None:
table_html_cached = cache_class.get(formsemestre.id)
if table_html_cached is None:
table = _gen_formsemestre_recapcomplet_table(
res,
include_evaluations,
@ -477,9 +484,14 @@ def gen_formsemestre_recapcomplet_html_table(
selected_etudid=selected_etudid,
)
table_html = table.html()
cache_class.set(formsemestre.id, table_html)
freq_codes_annuels = (
table.freq_codes_annuels if hasattr(table, "freq_codes_annuels") else None
)
cache_class.set(formsemestre.id, (table_html, freq_codes_annuels))
else:
table_html, freq_codes_annuels = table_html_cached
return table_html, table
return table_html, table, freq_codes_annuels
def _gen_formsemestre_recapcomplet_table(

View File

@ -145,12 +145,7 @@ class SemSet(dict):
# Construction du ou des lien(s) vers le semestre
self["semlinks"] = [
f"""<a class="stdlink" href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id)
}">{formsemestre.titre_annee()}</a>
"""
for formsemestre in self.formsemestres
formsemestre.html_link_status() for formsemestre in self.formsemestres
]
self["semtitles_str"] = "<br>".join(self["semlinks"])

View File

@ -15,7 +15,6 @@
padding-bottom: 0px;
padding-left: 16px;
padding-right: 0px;
background: #FFF;
border: 1px solid #aaa;
border-radius: 8px;
@ -39,4 +38,10 @@ div.code_rcue {
padding-top: 8px;
padding-bottom: 8px;
position: relative;
}
div.code_jury {
padding-right: 4px;
padding-left: 4px;
width: 64px;
}

View File

@ -0,0 +1,13 @@
div.jury_decisions_list div {
font-size: 120%;
font-weight: bold;
}
div.jury_decisions_list form {
display: inline-block;
}
span.parcours {
color:blueviolet;
}

View File

@ -2773,6 +2773,8 @@ table.notes_recapcomplet a:hover {
div.table_recap_caption {
width: fit-content;
margin-top: 8px;
margin-bottom: 8px;
padding: 8px;
border-radius: 8px;
background-color: rgb(202, 255, 180);

View File

@ -1,285 +1,311 @@
// Tableau recap notes
$(function () {
$(function () {
if ($('table.table_recap').length == 0) { return; }
$(function () {
if ($("table.table_recap").length == 0) {
return;
}
let hidden_colums = [
"etud_codes", "identite_detail",
"partition_aux", "partition_rangs", "admission",
"col_empty"
];
// Etat (tri des colonnes) de la table:
let hidden_colums = [
"etud_codes",
"identite_detail",
"partition_aux",
"partition_rangs",
"admission",
"col_empty",
];
// Etat (tri des colonnes) de la table:
const url = new URL(document.URL);
const formsemestre_id = url.searchParams.get("formsemestre_id");
const order_info_key = JSON.stringify([url.pathname, formsemestre_id]);
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
let order_info;
if (formsemestre_id) {
const x = localStorage.getItem(order_info_key);
if (x) {
try {
order_info = JSON.parse(x);
} catch (error) {
console.error(error);
}
}
}
// Les colonnes visibles sont mémorisées, il faut initialiser l'état des boutons
function update_buttons_labels(dt) {
// chaque bouton controle une classe stockée dans le data-group du span
document.querySelectorAll("button.dt-button").forEach(but => {
let g_span = but.querySelector("span > span");
if (g_span) {
let group = g_span.dataset["group"];
if (group) {
// si le group (= la 1ere col.) est visible, but_on
if (dt.columns("." + group).visible()[0]) {
but.classList.add("but_on");
but.classList.remove("but_off");
} else {
but.classList.add("but_off");
but.classList.remove("but_on");
}
}
}
});
}
// Changement visibilité groupes colonnes (boutons)
function toggle_col_but_visibility(e, dt, node, config) {
let group = node.children()[0].firstChild.dataset.group;
toggle_col_group_visibility(dt, group, node.hasClass("but_on"));
}
function toggle_col_ident_visibility(e, dt, node, config) {
let onoff = node.hasClass("but_on");
toggle_col_group_visibility(dt, "identite_detail", onoff);
toggle_col_group_visibility(dt, "identite_court", !onoff);
}
function toggle_col_ressources_visibility(e, dt, node, config) {
let onoff = node.hasClass("but_on");
toggle_col_group_visibility(dt, "col_res", onoff);
toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
toggle_col_group_visibility(dt, "col_malus", onoff);
}
function toggle_col_group_visibility(dt, group, onoff) {
if (onoff) {
dt.columns('.' + group).visible(false);
} else {
dt.columns('.' + group).visible(true);
}
update_buttons_labels(dt);
}
// Definition des boutons au dessus de la table:
let buttons = [
{
extend: 'copyHtml5',
text: 'Copier',
exportOptions: { orthogonal: 'export' }
},
{
extend: 'excelHtml5',
// footer: true, // ne fonctionne pas ?
exportOptions: { orthogonal: 'export' },
title: document.querySelector('table.table_recap').dataset.filename
},
{
// force affichage de toutes les colonnes
text: '<a title="Afficher toutes les colonnes">&#10036;</a>',
action: function (e, dt, node, config) {
dt.columns().visible(true);
update_buttons_labels(dt);
}
},
{
text: '<a title="Rétablir l\'affichage par défaut" class="clearreload">&#128260;</a>',
action: function (e, dt, node, config) {
localStorage.clear();
console.log("cleared localStorage");
location.reload();
}
},
{
text: '<span data-group="identite_detail">Civilité</span>',
action: toggle_col_ident_visibility,
},
{
text: '<span data-group="partition_aux"><a title="Affichage des groupes secondaires (la première partition est toujours affichée)">Groupes</a></span>',
action: toggle_col_but_visibility,
},
{
text: '<span data-group="partition_rangs"><a title="Rangs dans les groupes (si activés dans les partitions concernées)">Rg</a></span>',
action: toggle_col_but_visibility,
},
]; // fin des boutons communs à toutes les tables recap
if ($('table.table_recap').hasClass("jury")) {
// Table JURY:
// avec ou sans codes enregistrés
buttons.push(
{
text: '<span data-group="recorded_code">Codes jury</span>',
action: toggle_col_but_visibility,
});
if ($('table.table_recap').hasClass("apc")) {
// Boutons spécifiques à la table JURY BUT
buttons.push(
{
text: '<span data-group="cursus_but">Compétences</span>',
action: toggle_col_but_visibility,
});
buttons.push(
{
text: '<span data-group="col_rcue">RCUEs</span>',
action: toggle_col_but_visibility,
});
}
} else {
// BOUTONS SPECIFIQUES A LA TABLE RECAP NON JURY
buttons.push(
$('table.table_recap').hasClass("apc") ?
{
text: '<span data-group="col_res">Ressources</span>',
action: toggle_col_ressources_visibility,
} : {
name: "toggle_mod",
text: "Cacher les modules",
action: function (e, dt, node, config) {
let onoff = node.hasClass("but_on");
toggle_col_group_visibility(dt, "col_mod:not(.col_empty)", onoff);
toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
toggle_col_group_visibility(dt, "col_malus", onoff);
}
}
);
if ($('table.table_recap').hasClass("apc")) {
buttons.push({
text: '<span data-group="col_sae">SAÉs</span>',
action: toggle_col_but_visibility,
});
}
// S'il y a des colonnes vides:
if ($('table.table_recap td.col_empty').length > 0) {
buttons.push({ // modules vides
text: '<span data-group="col_empty">Vides</span>',
action: toggle_col_but_visibility,
});
}
// Boutons admission (pas en jury)
if (!$('table.table_recap').hasClass("jury")) {
buttons.push(
{
text: '<span data-group="admission">Admission</span>',
action: toggle_col_but_visibility,
}
);
}
}
// Boutons évaluations (si présentes)
if ($('table.table_recap').hasClass("with_evaluations")) {
buttons.push(
{
text: '<span data-group="eval">Évaluations</span>',
action: toggle_col_but_visibility,
}
);
}
// ------------- LA TABLE ---------
const url = new URL(document.URL);
const formsemestre_id = url.searchParams.get("formsemestre_id");
const order_info_key = JSON.stringify([url.pathname, formsemestre_id]);
const etudids_key = JSON.stringify([
"etudids",
url.origin,
formsemestre_id,
]);
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
let order_info;
if (formsemestre_id) {
const x = localStorage.getItem(order_info_key);
if (x) {
try {
let table = $('table.table_recap').DataTable(
{
paging: false,
searching: true,
info: false,
autoWidth: false,
fixedHeader: {
header: true,
footer: false
},
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
targets: hidden_colums,
visible: false,
},
{
// Elimine les 0 à gauche pour les exports excel et les "copy"
targets: ["col_mod", "col_moy_gen", "col_moy_ue", "col_res", "col_sae", "evaluation", "col_rcue"],
render: function (data, type, row) {
return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
}
},
{
// Elimine les "+"" pour les exports
targets: ["col_ue_bonus", "col_malus"],
render: function (data, type, row) {
return type === 'export' ? data.replace(/.*\+(\d?\d?\.\d\d).*/m, '$1').replace(/0(\d\..*)/, '$1') : data;
}
},
{
// Elimine emoji warning sur UEs
targets: ["col_ues_validables"],
render: function (data, type, row) {
return type === 'export' ? data.replace(/(\d+\/\d+).*/, '$1') : data;
}
}
],
dom: 'Bfrtip',
buttons: buttons,
"drawCallback": function (settings) {
// permet de conserver l'ordre de tri des colonnes
let table = $('table.table_recap').DataTable();
let order_info = JSON.stringify(table.order());
if (formsemestre_id) {
localStorage.setItem(order_info_key, order_info);
}
let etudids = [];
document.querySelectorAll("td.identite_court").forEach(e => {
etudids.push(e.dataset.etudid);
});
let noms = [];
document.querySelectorAll("td.identite_court").forEach(e => {
noms.push(e.dataset.nomprenom);
});
localStorage.setItem(etudids_key, JSON.stringify(etudids));
localStorage.setItem(noms_key, JSON.stringify(noms));
},
"order": order_info,
}
);
update_buttons_labels(table);
order_info = JSON.parse(x);
} catch (error) {
// l'erreur peut etre causee par un ancien storage:
localStorage.removeItem(etudids_key);
localStorage.removeItem(noms_key);
localStorage.removeItem(order_info_key);
location.reload();
console.error(error);
}
});
$('table.table_recap tbody').on('click', 'tr', function () {
if ($(this).hasClass('selected')) {
$(this).removeClass('selected');
}
}
// Les colonnes visibles sont mémorisées, il faut initialiser l'état des boutons
function update_buttons_labels(dt) {
// chaque bouton controle une classe stockée dans le data-group du span
document.querySelectorAll("button.dt-button").forEach((but) => {
let g_span = but.querySelector("span > span");
if (g_span) {
let group = g_span.dataset["group"];
if (group) {
// si le group (= la 1ere col.) est visible, but_on
if (dt.columns("." + group).visible()[0]) {
but.classList.add("but_on");
but.classList.remove("but_off");
} else {
but.classList.add("but_off");
but.classList.remove("but_on");
}
}
}
else {
$('table.table_recap tr.selected').removeClass('selected');
$(this).addClass('selected');
}
});
// Pour montrer et surligner l'étudiant sélectionné:
$(function () {
let row_selected = document.querySelector(".row_selected");
if (row_selected) {
row_selected.scrollIntoView();
window.scrollBy(0, -125);
row_selected.classList.add("selected");
}
});
});
}
// Changement visibilité groupes colonnes (boutons)
function toggle_col_but_visibility(e, dt, node, config) {
let group = node.children()[0].firstChild.dataset.group;
toggle_col_group_visibility(dt, group, node.hasClass("but_on"));
}
function toggle_col_ident_visibility(e, dt, node, config) {
let onoff = node.hasClass("but_on");
toggle_col_group_visibility(dt, "identite_detail", onoff);
toggle_col_group_visibility(dt, "identite_court", !onoff);
}
function toggle_col_ressources_visibility(e, dt, node, config) {
let onoff = node.hasClass("but_on");
toggle_col_group_visibility(dt, "col_res", onoff);
toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
toggle_col_group_visibility(dt, "col_malus", onoff);
}
function toggle_col_group_visibility(dt, group, onoff) {
if (onoff) {
dt.columns("." + group).visible(false);
} else {
dt.columns("." + group).visible(true);
}
update_buttons_labels(dt);
}
// Definition des boutons au dessus de la table:
let buttons = [
{
extend: "copyHtml5",
text: "Copier",
exportOptions: { orthogonal: "export" },
},
{
extend: "excelHtml5",
// footer: true, // ne fonctionne pas ?
exportOptions: { orthogonal: "export" },
title: document.querySelector("table.table_recap").dataset.filename,
},
{
// force affichage de toutes les colonnes
text: '<a title="Afficher toutes les colonnes">&#10036;</a>',
action: function (e, dt, node, config) {
dt.columns().visible(true);
update_buttons_labels(dt);
},
},
{
text: '<a title="Rétablir l\'affichage par défaut" class="clearreload">&#128260;</a>',
action: function (e, dt, node, config) {
localStorage.clear();
console.log("cleared localStorage");
location.reload();
},
},
{
text: '<span data-group="identite_detail">Civilité</span>',
action: toggle_col_ident_visibility,
},
{
text: '<span data-group="partition_aux"><a title="Affichage des groupes secondaires (la première partition est toujours affichée)">Groupes</a></span>',
action: toggle_col_but_visibility,
},
{
text: '<span data-group="partition_rangs"><a title="Rangs dans les groupes (si activés dans les partitions concernées)">Rg</a></span>',
action: toggle_col_but_visibility,
},
]; // fin des boutons communs à toutes les tables recap
if ($("table.table_recap").hasClass("jury")) {
// Table JURY:
// avec ou sans codes enregistrés
buttons.push({
text: '<span data-group="recorded_code">Codes jury</span>',
action: toggle_col_but_visibility,
});
if ($("table.table_recap").hasClass("apc")) {
// Boutons spécifiques à la table JURY BUT
buttons.push({
text: '<span data-group="cursus_but">Compétences</span>',
action: toggle_col_but_visibility,
});
buttons.push({
text: '<span data-group="col_rcue">RCUEs</span>',
action: toggle_col_but_visibility,
});
}
} else {
// BOUTONS SPECIFIQUES A LA TABLE RECAP NON JURY
buttons.push(
$("table.table_recap").hasClass("apc")
? {
text: '<span data-group="col_res">Ressources</span>',
action: toggle_col_ressources_visibility,
}
: {
name: "toggle_mod",
text: "Cacher les modules",
action: function (e, dt, node, config) {
let onoff = node.hasClass("but_on");
toggle_col_group_visibility(
dt,
"col_mod:not(.col_empty)",
onoff
);
toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
toggle_col_group_visibility(dt, "col_malus", onoff);
},
}
);
if ($("table.table_recap").hasClass("apc")) {
buttons.push({
text: '<span data-group="col_sae">SAÉs</span>',
action: toggle_col_but_visibility,
});
}
// S'il y a des colonnes vides:
if ($("table.table_recap td.col_empty").length > 0) {
buttons.push({
// modules vides
text: '<span data-group="col_empty">Vides</span>',
action: toggle_col_but_visibility,
});
}
// Boutons admission (pas en jury)
if (!$("table.table_recap").hasClass("jury")) {
buttons.push({
text: '<span data-group="admission">Admission</span>',
action: toggle_col_but_visibility,
});
}
}
// Boutons évaluations (si présentes)
if ($("table.table_recap").hasClass("with_evaluations")) {
buttons.push({
text: '<span data-group="eval">Évaluations</span>',
action: toggle_col_but_visibility,
});
}
// ------------- LA TABLE ---------
try {
let table = $("table.table_recap").DataTable({
paging: false,
searching: true,
info: false,
autoWidth: false,
fixedHeader: {
header: true,
footer: false,
},
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
targets: hidden_colums,
visible: false,
},
{
// Elimine les 0 à gauche pour les exports excel et les "copy"
targets: [
"col_mod",
"col_moy_gen",
"col_moy_ue",
"col_res",
"col_sae",
"evaluation",
"col_rcue",
],
render: function (data, type, row) {
return type === "export" ? data.replace(/0(\d\..*)/, "$1") : data;
},
},
{
// Elimine les "+"" pour les exports
targets: ["col_ue_bonus", "col_malus"],
render: function (data, type, row) {
return type === "export"
? data
.replace(/.*\+(\d?\d?\.\d\d).*/m, "$1")
.replace(/0(\d\..*)/, "$1")
: data;
},
},
{
// Elimine emoji warning sur UEs
targets: ["col_ues_validables"],
render: function (data, type, row) {
return type === "export"
? data.replace(/(\d+\/\d+).*/, "$1")
: data;
},
},
],
dom: "Bfrtip",
buttons: buttons,
drawCallback: function (settings) {
// permet de conserver l'ordre de tri des colonnes
let table = $("table.table_recap").DataTable();
let order_info = JSON.stringify(table.order());
if (formsemestre_id) {
localStorage.setItem(order_info_key, order_info);
}
let etudids = [];
document.querySelectorAll("td.identite_court").forEach((e) => {
etudids.push(e.dataset.etudid);
});
let noms = [];
document.querySelectorAll("td.identite_court").forEach((e) => {
noms.push(e.dataset.nomprenom);
});
localStorage.setItem(etudids_key, JSON.stringify(etudids));
localStorage.setItem(noms_key, JSON.stringify(noms));
},
order: order_info,
});
update_buttons_labels(table);
} catch (error) {
// l'erreur peut etre causee par un ancien storage:
localStorage.removeItem(etudids_key);
localStorage.removeItem(noms_key);
localStorage.removeItem(order_info_key);
location.reload();
}
});
$("table.table_recap tbody").on("click", "tr", function () {
if ($(this).hasClass("selected")) {
$(this).removeClass("selected");
} else {
$("table.table_recap tr.selected").removeClass("selected");
$(this).addClass("selected");
}
});
// Pour montrer et surligner l'étudiant sélectionné:
$(function () {
let row_selected = document.querySelector(".row_selected");
if (row_selected) {
row_selected.scrollIntoView();
window.scrollBy(0, -125);
row_selected.classList.add("selected");
}
});
// Ajoute bulle aide sur colonne RCUEs
$(function () {
// explication colonne RCUEs
let th = document.querySelector(
"table.table_recap.apc th.col_rcues_validables"
);
th.title = "RCUEs validables avec ces notes";
});
});

View File

@ -78,14 +78,18 @@ class TableJury(TableRecap):
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
row.add_rcue_cols(dec_rcue)
self.freq_codes_annuels["total"] = len(self.rows)
def add_jury(self):
"""Ajoute la colonne code jury et le lien.
Le code jury est celui du semestre: cette colonne n'est montrée
- Le code jury est celui du semestre: cette colonne n'est montrée
que pour les formations classiques, ce code n'est pas utilisé en BUT.
- En BUT, on donne la décision de jury annuelle.
"""
res = self.res
autorisations = res.get_autorisations_inscription()
if res.is_apc:
validations_annee = res.get_validations_annee()
for row in self.rows:
etud = row.etud
if not res.is_apc:
@ -115,6 +119,15 @@ class TableJury(TableRecap):
group="jury_code_sem",
classes=["recorded_code"],
)
if res.is_apc: # BUT
validation_annee = validations_annee.get(etud.id, None)
row.add_cell(
"decision_annuelle",
"Année",
validation_annee.code if validation_annee else "",
group="jury_code_sem",
classes=["recorded_code"],
)
# Lien saisie ou visu jury
a_saisir = (not res.validations) or (not res.validations.has_decision(etud))
row.add_cell(

View File

@ -15,11 +15,9 @@
<input type="hidden" name="etudid" value="{{etud.id}}"></input>
<input type="hidden" name="format" value="{{format}}"></input>
Bulletin
<span class="bull_liensemestre"><a href="{{
url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id)}}">{{formsemestre.titre_mois()
}}</a></span>
<span class="bull_liensemestre">
{{formsemestre.html_link_status() | safe}}
</span>
<div>
<em>établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20)</em>

View File

@ -15,14 +15,16 @@
<div class="code_jury">{{validation.code}}</div>
<div class="scoplement">
<div>{{validation.ue1.acronyme}} - {{validation.ue2.acronyme}}</div>
<div>Jury de {{validation.formsemestre.titre_annee()}}</div>
<div>Jury de {{validation.formsemestre.titre_annee() if validation.formsemestre else "-"}}</div>
<div>enregistré le {{
validation.date.strftime("%d/%m/%Y à %H:%M")
}}</div>
</div>
</div>
{% else %}
-
<div class="code_rcue">
<div class="code_jury">-</div>
</div>
{%endif%}
</div>
{% endfor %}

View File

@ -26,9 +26,13 @@
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
notamment sur les UEs en dessous de 10.
</p>
<p class="warning">
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
</p>
<div class="warning">
<ul>
<li>Ne jamais lancer ce calcul avant que toutes les notes ne soient saisies !
(verrouiller le semestre ensuite)
</li>
<li>Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !</li>
</div>
<div class="row">

View File

@ -44,7 +44,7 @@
{%- endmacro %}
{% block app_content %}
<h2>{{formation.to_html()}}</h2>
<h2>{{formation.html()}}</h2>
{# Liens vers les différents parcours #}
<div class="les_parcours">
@ -127,7 +127,7 @@ Choisissez un parcours...
d'associer à chaque semestre d'un niveau de compétence une UE de la formation
<a class="stdlink"
href="{{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}">{{formation.to_html()}}
}}">{{formation.html()}}
</a>.</p>
<p>Le symbole <span class="parc">TC</span> désigne un niveau du tronc commun

View File

@ -0,0 +1,41 @@
{% extends 'base.j2' %}
{% block app_content %}
{% if not validations %}
<p>Aucune validation de jury enregistrée pour <b>{{etud.html_link_fiche()|safe}}</b>
sur <b>l'année {{annee}}</b>
de la formation <em>{{ formation.html() }}</em>
</p>
<div style="margin-top: 16px;">
<a class="stdlink" href="{{ cancel_url }}">continuer</a>
</div>
{% else %}
<h2>Effacer les décisions de jury pour l'année {{annee}} de {{etud.html_link_fiche()|safe}} ?</h2>
<p class="help">Affectera toutes les décisions concernant l'année {{annee}} de la formation,
quelle que soit leur origine.</p>
<p>Les décisions concernées sont:</p>
<ul>
{% for validation in validations %}
<li>{{ validation.html() | safe}}
</li>
{% endfor %}
</ul>
<div style="margin-top: 16px;">
<form method="post">
<input type="submit" value="Effacer ces décisions" />
{% if cancel_url %}
<input type="button" value="Annuler" style="margin-left: 16px;"
onClick="document.location='{{ cancel_url }}';" />
{% endif %}
</form>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,135 @@
{% extends 'base.j2' %}
{% block styles %}
{{super()}}
<link href="{{scu.STATIC_DIR}}/css/jury_delete_manual.css" rel="stylesheet" type="text/css" />
{% endblock %}
{% block app_content %}
<h2>Décisions de jury enregistrées pour {{etud.html_link_fiche()|safe}}</h2>
<p class="help">
Cette page liste toutes les décisions de jury connus de ScoDoc concernant cet étudiant
et permet de les effacer une par une.
</p>
<p class="help">
<b>Attention</b>, il vous appartient de vérifier la cohérence du résultat !
En principe, <b>l'usage de cette page devrait rester exceptionnel</b>.
Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les
pages de saisie de jury habituelles).
</p>
{% if sem_vals.first() %}
<div class="jury_decisions_list jury_decisions_sems">
<div>Décisions de semestres</div>
<ul>
{% for v in sem_vals %}
<li>{{v.html()|safe}}
<form><button data-v_id="{{v.id}}" data-type="validation_formsemestre">effacer</button></form>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if ue_vals.first() %}
<div class="jury_decisions_list jury_decisions_ues">
<div>Décisions d'UEs</div>
<ul>
{% for v in ue_vals %}
<li>{{v.html(detail=True)|safe}}
<form><button data-v_id="{{v.id}}" data-type="validation_ue">effacer</button></form>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if rcue_vals.first() %}
<div class="jury_decisions_list jury_decisions_rcues">
<div>Décisions de RCUE (niveaux de compétences)</div>
<ul>
{% for v in rcue_vals %}
<li>{{v.html()|safe}}
<form><button data-v_id="{{v.id}}" data-type="validation_rcue">effacer</button></form>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if annee_but_vals.first() %}
<div class="jury_decisions_list jury_decisions_annees_but">
<div>Décisions d'années BUT</div>
<ul>
{% for v in annee_but_vals %}
<li>{{v.html()|safe}}
<form><button data-v_id="{{v.id}}" data-type="validation_annee_but">effacer</button></form>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if autorisations.first() %}
<div class="jury_decisions_list jury_decisions_autorisation_inscription">
<div>Autorisations d'inscriptions (passages)</div>
<ul>
{% for v in autorisations %}
<li>{{v.html()|safe}}
<form><button data-v_id="{{v.id}}" data-type="autorisation_inscription">effacer</button></form>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if not(
sem_vals.first() or ue_vals.first() or rcue_vals.first()
or annee_but_vals.first() or autorisations.first())
%}
<div>
<p class="fontred">aucune décision enregistrée</p>
</div>
{% endif %}
<div>
<p>retour à la fiche de {{etud.html_link_fiche()|safe}}
</p>
</div>
{% endblock %}
{% block scripts %}
{{super()}}
<script>
document.addEventListener('DOMContentLoaded', () => {
const buttons = document.querySelectorAll('.jury_decisions_list button');
buttons.forEach(button => {
button.addEventListener('click', (event) => {
// Handle button click event here
event.preventDefault();
const v_id = event.target.dataset.v_id;
const validation_type = event.target.dataset.type;
if (confirm("Supprimer cette validation ?")) {
fetch(`${SCO_URL}/../api/etudiant/{{etud.id}}/jury/${validation_type}/${v_id}/delete`,
{
method: "POST",
}).then(response => {
// Handle the response
if (response.ok) {
location.reload();
} else {
throw new Error('Request failed');
}
});
}
});
});
});
</script>
{% endblock %}

View File

@ -2897,7 +2897,12 @@ def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
)
+ """
<p>Les décisions des années scolaires précédentes ne seront pas modifiées.</p>
<div class="warning">Cette opération est irréversible !</div>
<p>Efface aussi toutes les validations concernant l'année BUT de ce semestre,
même si elles ont été acquises ailleurs.
</p>
<div class="warning">Cette opération est irréversible !
A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite.
</div>
""",
cancel_url=dest_url,
)

View File

@ -0,0 +1,63 @@
"""validation niveaux inferieurs
Revision ID: c701224fa255
Revises: d84bc592584e
Create Date: 2023-06-11 11:08:05.553898
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic.
revision = "c701224fa255"
down_revision = "d84bc592584e"
branch_labels = None
depends_on = None
Session = sessionmaker()
def upgrade():
# Ajoute la colonne formation_id, nullable, la peuple puis la rend non nullable
op.add_column(
"apc_validation_annee", sa.Column("formation_id", sa.Integer(), nullable=True)
)
op.create_foreign_key(
"apc_validation_annee_formation_id_fkey",
"apc_validation_annee",
"notes_formations",
["formation_id"],
["id"],
)
# Affecte la formation des anciennes validations
bind = op.get_bind()
session = Session(bind=bind)
session.execute(
sa.text(
"""
UPDATE apc_validation_annee AS a
SET formation_id = (
SELECT f.id
FROM notes_formations f
JOIN notes_formsemestre s ON f.id = s.formation_id
WHERE s.id = a.formsemestre_id
)
WHERE a.formsemestre_id IS NOT NULL;
"""
)
)
op.alter_column(
"apc_validation_annee",
"formation_id",
nullable=False,
)
def downgrade():
with op.batch_alter_table("apc_validation_annee", schema=None) as batch_op:
batch_op.drop_constraint(
"apc_validation_annee_formation_id_fkey", type_="foreignkey"
)
batch_op.drop_column("formation_id")

View File

@ -25,7 +25,8 @@ def upgrade():
bind = op.get_bind()
session = Session(bind=bind)
# Ajout extension pour recherches sans accents:
session.execute(sa.text("""CREATE EXTENSION IF NOT EXISTS "unaccent";"""))
# erreur: doit s'executer en superuser
# session.execute(sa.text("""CREATE EXTENSION IF NOT EXISTS "unaccent";"""))
# Clé étrangère sur identite
session.execute(

View File

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

View File

@ -60,6 +60,7 @@ def test_permissions(api_headers):
"role_name": "Ens",
"start": "abc",
"uid": 1,
"validation_id": 1,
"version": "long",
}
for rule in api_rules:

View File

@ -1,122 +1,122 @@
# Tests unitaires jury BUT
# Essais avec un BUT GCCD (GC-CD) et un parcours de S1 à S6
# Le GCCD est un programme à 5 compétences, dont certaines
# Le GCCD est un programme à 5 compétences, dont certaines
# terminent en S4 ou en S6 selon les parcours.
ReferentielCompetences:
filename: but-GCCD-05012022-081630.xml
filename: but-GCCD-05012022-081630.xml
specialite: GCCD
Formation:
filename: scodoc_formation_BUT_GC-CD_v2.xml
# Association des UEs aux compétences:
ues:
# S1 tronc commun:
'UE1.1':
# S1 tronc commun:
"UE1.1":
annee: BUT1
competence: "Solutions Bâtiment"
'UE1.2':
"UE1.2":
annee: BUT1
competence: "Solutions TP"
'UE1.3':
"UE1.3":
annee: BUT1
competence: "Dimensionner"
'UE1.4':
"UE1.4":
annee: BUT1
competence: Organiser
'UE1.5':
"UE1.5":
annee: BUT1
competence: Piloter
# S2 tronc commun:
'UE2.1':
# S2 tronc commun:
"UE2.1":
annee: BUT1
competence: "Solutions Bâtiment"
'UE2.2':
"UE2.2":
annee: BUT1
competence: "Solutions TP"
'UE2.3':
"UE2.3":
annee: BUT1
competence: "Dimensionner"
'UE2.4':
"UE2.4":
annee: BUT1
competence: Organiser
'UE2.5':
"UE2.5":
annee: BUT1
competence: Piloter
# S3 : Tronc commun
'UE3.1':
"UE3.1":
annee: BUT2
competence: "Solutions Bâtiment"
'UE3.2':
"UE3.2":
annee: BUT2
competence: "Solutions TP"
'UE3.3':
"UE3.3":
annee: BUT2
competence: "Dimensionner"
'UE3.4':
"UE3.4":
annee: BUT2
competence: Organiser
'UE3.5':
"UE3.5":
annee: BUT2
competence: Piloter
# S4 Tronc commun
'UE4.1':
"UE4.1":
annee: BUT2
competence: "Solutions Bâtiment"
'UE4.2':
"UE4.2":
annee: BUT2
competence: "Solutions TP"
'UE4.3':
"UE4.3":
annee: BUT2
competence: "Dimensionner"
'UE4.4':
"UE4.4":
annee: BUT2
competence: Organiser
'UE4.5':
"UE4.5":
annee: BUT2
competence: Piloter
# S5 Parcours BAT + TP
'UE5.1': # Parcours BAT seulement
"UE5.1": # Parcours BAT seulement
annee: BUT3
parcours: BAT # + RAPEB, BEC
competence: "Solutions Bâtiment"
'UE5.2': # Parcours TP seulement
"UE5.2": # Parcours TP seulement
annee: BUT3
parcours: TP # + BEC
competence: "Solutions TP"
'UE5.3':
"UE5.3":
annee: BUT3
parcours: [RAPEB, BEC]
competence: "Dimensionner"
'UE5.4':
"UE5.4":
annee: BUT3
parcours: [BAT, TP]
competence: Organiser
'UE5.5':
"UE5.5":
annee: BUT3
parcours: [BAT, TP]
competence: Piloter
# S6 Parcours BAT + TP
'UE6.1': # Parcours BAT seulement
"UE6.1": # Parcours BAT seulement
annee: BUT3
parcours: BAT # + RAPEB, BEC
competence: "Solutions Bâtiment"
'UE6.2': # Parcours TP seulement
"UE6.2": # Parcours TP seulement
annee: BUT3
parcours: [TP,BEC]
parcours: [TP, BEC]
competence: "Solutions TP"
'UE6.3':
"UE6.3":
annee: BUT3
parcours: [RAPEB,BEC]
parcours: [RAPEB, BEC]
competence: "Dimensionner"
'UE6.4':
"UE6.4":
annee: BUT3
parcours: [BAT, TP]
competence: Organiser
'UE6.5':
"UE6.5":
annee: BUT3
parcours: [BAT,TP]
parcours: [BAT, TP]
competence: Piloter
modules_parcours:
@ -126,8 +126,8 @@ Formation:
# - tous les module de S1 à S4 dans tous les parcours
# - SAE communes en S1 et S2 mais différenciées par parcours ensuite
# - en S5, ressources différenciées: on ne les mentionne pas toutes ici
BAT: [ "R[1-4].*", "SAÉ [1-2]", "SAÉ *.BAT.*", "R5.0[1-7]", "R5.14" ]
TP: [ "R[1-4].*", "SAÉ [1-2]", "SAÉ *.TP.*", "R5.0[1-4]", "R5.0[89]" ]
BAT: ["R[1-4].*", "SAÉ [1-2]", "SAÉ *.BAT.*", "R5.0[1-7]", "R5.14"]
TP: ["R[1-4].*", "SAÉ [1-2]", "SAÉ *.TP.*", "R5.0[1-4]", "R5.0[89]"]
FormSemestres:
# S1 et S2 avec les parcours BAT et TP:
@ -135,32 +135,32 @@ FormSemestres:
idx: 1
date_debut: 2021-09-01
date_fin: 2022-01-15
codes_parcours: ['BAT', 'TP']
S2:
codes_parcours: ["BAT", "TP"]
S2:
idx: 2
date_debut: 2022-01-15
date_fin: 2022-06-30
codes_parcours: ['BAT', 'TP']
codes_parcours: ["BAT", "TP"]
S3:
idx: 3
date_debut: 2022-09-01
date_fin: 2023-01-15
codes_parcours: ['BAT', 'TP']
codes_parcours: ["BAT", "TP"]
S4:
idx: 4
date_debut: 2023-01-16
date_fin: 2023-06-30
codes_parcours: ['BAT', 'TP']
codes_parcours: ["BAT", "TP"]
S5:
idx: 5
date_debut: 2023-09-01
date_fin: 2024-01-15
codes_parcours: ['BAT', 'TP']
codes_parcours: ["BAT", "TP"]
S6:
idx: 6
date_debut: 2024-01-16
date_fin: 2024-06-30
codes_parcours: ['BAT', 'TP']
codes_parcours: ["BAT", "TP"]
Etudiants:
A_ok: # Etudiant parcours BAT qui va tout valider directement
@ -171,10 +171,18 @@ Etudiants:
parcours: BAT
notes_modules:
"R1.01": 11 # toutes UEs
"SAÉ 1-2": EXC
S2:
parcours: BAT
notes_modules:
"R2.01": 12 # toutes UEs
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: True
autorisations_inscription: [3]
code_valide:
nb_competences: 5
nb_rcue_annee: 4
S3:
parcours: BAT
notes_modules:
@ -186,7 +194,7 @@ Etudiants:
S5:
parcours: BAT
dispense_ues: ['UE5.2', 'UE5.3']
dispense_ues: ["UE5.2", "UE5.3"]
notes_modules:
"R5.01": 15 # toutes UE
"SAÉ 5.BAT.01": 10 # UE5.1
@ -202,6 +210,7 @@ Etudiants:
parcours: TP
notes_modules:
"R1.01": 11 # toutes UEs
"SAÉ 1-2": EXC
S2:
parcours: TP
notes_modules:
@ -217,10 +226,55 @@ Etudiants:
S5:
parcours: TP
dispense_ues: ['UE5.1', 'UE5.3']
dispense_ues: ["UE5.1", "UE5.3"]
notes_modules:
"R5.01": 15 # toutes UE
"SAÉ 5.BAT.01": 10 # UE5.1
"SAÉ 5.BAT.02": 11 # UE5.4
S6:
parcours: TP
C: # Etudiant qui passe sans un RCUE et valide en BUT2
prenom: Étudiant_TP_but2
civilite: M
formsemestres:
S1:
parcours: TP
notes_modules:
"R1.01": 11 # toutes UEs
"SAÉ 1-2": 8 # plombe l'UE 2
S2:
parcours: TP
notes_modules:
"R2.01": 11 # toutes UEs
S3:
parcours: TP
notes_modules:
"R3.01": 12 # toutes UEs
S4:
parcours: TP
notes_modules:
"R4.01": 14 # toutes UE
D: # Etudiant arrive en S4 avec une UE manquante en S1
prenom: Étudiant_TP_malaise
civilite: M
formsemestres:
S1:
parcours: TP
notes_modules:
"R1.01": 11 # toutes UEs
"SAÉ 1-2": 8 # plombe l'UE 2
S2:
parcours: TP
notes_modules:
"R2.01": 11 # toutes UEs
S3:
parcours: TP
notes_modules:
"R3.01": 12 # toutes UEs
S4:
parcours: TP
notes_modules:
"R4.01": 14 # toutes UE
"R4.04": 6 # plombe l'UE1

View File

@ -146,7 +146,7 @@ def create_formsemestre(
return formsemestre
def create_evaluations(formsemestre: FormSemestre):
def create_evaluations(formsemestre: FormSemestre, publish_incomplete=True):
"""Crée une évaluation dans chaque module du semestre"""
for modimpl in formsemestre.modimpls:
evaluation = Evaluation(
@ -156,6 +156,7 @@ def create_evaluations(formsemestre: FormSemestre):
coefficient=1.0,
note_max=20.0,
numero=1,
publish_incomplete=publish_incomplete,
)
db.session.add(evaluation)

View File

@ -31,4 +31,4 @@ source "$SCRIPT_DIR"/utils.sh || die "config.sh not found, exiting"
# ---
echo 'Creating postgresql database ' "$db_name"
createdb -E UTF-8 -p "$POSTGRES_PORT" -O "$POSTGRES_USER" "$db_name"
echo 'CREATE EXTENSION IF NOT EXISTS "unaccent";' | psql -p "$POSTGRES_PORT" "$db_name" "$POSTGRES_USER"

View File

@ -97,13 +97,15 @@ fi
init_postgres_user
# ------------ BASE DE DONNEES
# gérées avec Flask-Migrate (Alembic/SQLAlchemy)
# Si la base SCODOC existe, tente de la mettre à jour
# (Ne gère pas les bases DEV et TEST)
n=$(su -c "psql -l | grep -c -E '^[[:blank:]]*SCODOC[[:blank:]]*\|'" "$SCODOC_USER")
if [ "$n" == 1 ]
then
echo "Upgrading existing SCODOC database..."
# Ajout extension unaccent (postgres superuser, ajout sur base SCODOC)
(cd /tmp; echo 'CREATE EXTENSION IF NOT EXISTS "unaccent";' | su -c "psql SCODOC" postgres)
# Migrations gérées avec Flask-Migrate (Alembic/SQLAlchemy)
# utilise les scripts dans migrations/version/
# pour mettre à jour notre base (en tant qu'utilisateur scodoc)
export FLASK_ENV="production"