diff --git a/app/api/jury.py b/app/api/jury.py
index 6103d116..6f071077 100644
--- a/app/api/jury.py
+++ b/app/api/jury.py
@@ -114,16 +114,16 @@ def _validation_ue_delete(etudid: int, validation_id: int):
# rattachées à un formsemestre)
if not g.scodoc_dept: # accès API
if not current_user.has_permission(Permission.ScoEtudInscrit):
- return json_error(403, "validation_delete: non autorise")
+ return json_error(403, "opération non autorisée (117)")
else:
if validation.formsemestre:
if (
validation.formsemestre.dept_id != g.scodoc_dept_id
) or not validation.formsemestre.can_edit_jury():
- return json_error(403, "validation_delete: non autorise")
+ return json_error(403, "opération non autorisée (123)")
elif not current_user.has_permission(Permission.ScoEtudInscrit):
# Validation non rattachée à un semestre: on doit être chef
- return json_error(403, "validation_delete: non autorise")
+ return json_error(403, "opération non autorisée (126)")
log(f"validation_ue_delete: etuid={etudid} {validation}")
db.session.delete(validation)
diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py
index 5eda000d..97a555ca 100644
--- a/app/but/cursus_but.py
+++ b/app/but/cursus_but.py
@@ -43,7 +43,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 code_ue_validant, RED, UE_STANDARD
+from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
@@ -102,7 +102,7 @@ class EtudCursusBUT:
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
self.parcour: ApcParcours = self.inscriptions[-1].parcour
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
- self.niveaux_by_annee = {}
+ self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
"{ annee:int : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
@@ -364,10 +364,33 @@ class FormSemestreCursusBUT:
"cache { competence_id : competence }"
+def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
+ """Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué.
+ Ne prend que les UE associées à des niveaux de compétences,
+ et ne les compte qu'une fois même en cas de redoublement avec re-validation.
+ """
+ validations = (
+ ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
+ .filter(ScolarFormSemestreValidation.ue_id != None)
+ .join(UniteEns)
+ .join(ApcNiveau)
+ .join(ApcCompetence)
+ .filter_by(referentiel_id=referentiel_competence_id)
+ )
+
+ ects_dict = {}
+ for v in validations:
+ key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
+ if v.code in CODES_UE_VALIDES:
+ ects_dict[key] = v.ue.ects
+
+ return sum(ects_dict.values()) if ects_dict else 0.0
+
+
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"""
+ """Liste des UEs de S1 et S2 non validées, 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)
@@ -377,9 +400,9 @@ def etud_ues_de_but1_non_validees(
.join(Formation)
.filter_by(formation_code=formation.formation_code)
)
- codes_validations_by_ue = collections.defaultdict(list)
+ codes_validations_by_ue_code = collections.defaultdict(list)
for v in validations:
- codes_validations_by_ue[v.ue_id].append(v.code)
+ codes_validations_by_ue_code[v.ue.ue_code].append(v.code)
# Les UEs du parcours en S1 et S2:
ues = formation.query_ues_parcour(parcour).filter(
@@ -390,8 +413,11 @@ def etud_ues_de_but1_non_validees(
[
ue
for ue in ues
- if any(
- (not code_ue_validant(code) for code in codes_validations_by_ue[ue.id])
+ if not any(
+ (
+ code_ue_validant(code)
+ for code in codes_validations_by_ue_code[ue.ue_code]
+ )
)
],
key=attrgetter("numero", "acronyme"),
diff --git a/app/but/jury_but.py b/app/but/jury_but.py
index 97ab95ac..1ec6eeea 100644
--- a/app/but/jury_but.py
+++ b/app/but/jury_but.py
@@ -284,15 +284,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# ---- Décision année et autorisation
self.autorisations_recorded = False
"vrai si on a enregistré l'autorisation de passage"
- self.validation = (
- ApcValidationAnnee.query.filter_by(
- etudid=self.etud.id,
- ordre=self.annee_but,
- )
- .join(Formation)
- .filter_by(formation_code=self.formsemestre.formation.formation_code)
- .first()
- )
+ self.validation = ApcValidationAnnee.query.filter_by(
+ etudid=self.etud.id,
+ ordre=self.annee_but,
+ referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
+ ).first()
"Validation actuellement enregistrée pour cette année BUT"
self.code_valide = self.validation.code if self.validation is not None else None
"Le code jury annuel enregistré, ou None"
@@ -346,21 +342,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Cas particulier du passage en BUT 3: nécessité d'avoir 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, self.formsemestre.formation, self.parcour
- )
- self.passage_de_droit = not ues_but1_non_validees
- explanation += (
- f"""UEs de BUT1 non validées: {
- ', '.join(ue.acronyme for ue in ues_but1_non_validees)
- }. """
- if ues_but1_non_validees
- else ""
- )
- else:
+ if not inscription or inscription.etat != scu.INSCRIT:
# pas inscrit dans le semestre courant ???
self.passage_de_droit = False
+ else:
+ self.passage_de_droit, explanation = self.passage_de_droit_en_but3()
# Enfin calcule les codes des UEs:
for dec_ue in self.decisions_ues.values():
@@ -427,6 +413,53 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
self.codes = [self.codes[0]] + sorted(self.codes[1:])
+ def passage_de_droit_en_but3(self) -> tuple[bool, str]:
+ """Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
+ cursus: EtudCursusBUT = EtudCursusBUT(self.etud, self.formsemestre.formation)
+ niveaux_but1 = cursus.niveaux_by_annee[1]
+
+ niveaux_but1_non_valides = []
+ for niveau in niveaux_but1:
+ ok = False
+ validation_par_annee = cursus.validation_par_competence_et_annee.get(
+ niveau.competence_id
+ )
+ if validation_par_annee:
+ validation_niveau = validation_par_annee.get("BUT1")
+ if validation_niveau and validation_niveau.code in CODES_RCUE_VALIDES:
+ ok = True
+ if not ok:
+ niveaux_but1_non_valides.append(niveau)
+
+ # Les niveaux de BUT1 manquants passent-ils en ADSUP ?
+ # en vertu de l'article 4.3,
+ # "La validation des deux UE du niveau d’une compétence emporte la validation de
+ # l’ensemble des UE du niveau inférieur de cette même compétence."
+ explanation = ""
+ ok = True
+ for niveau_but1 in niveaux_but1_non_valides:
+ niveau_but2 = niveau_but1.competence.niveaux.filter_by(annee="BUT2").first()
+ if niveau_but2:
+ rcue = self.rcue_by_niveau.get(niveau_but2.id)
+ if (rcue is None) or (
+ not rcue.est_validable() and not rcue.code_valide()
+ ):
+ # le RCUE de BUT2 n'est ni validable (avec les notes en cours) ni déjà validé
+ ok = False
+ explanation += (
+ f"Compétence {niveau_but1} de BUT 1 non validée.
"
+ )
+ else:
+ explanation += (
+ f"Compétence {niveau_but1} de BUT 1 validée par ce BUT2.
"
+ )
+ else:
+ ok = False
+ explanation += f"""Compétence {
+ niveau_but1} de BUT 1 non validée et non existante en BUT2.
"""
+
+ return ok, explanation
+
# WIP TODO XXX def get_moyenne_annuelle(self)
def infos(self) -> str:
@@ -689,7 +722,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
- formation_id=self.formsemestre.formation_id,
+ referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
@@ -852,13 +885,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Efface les validations concernant l'année BUT
# de ce semestre
- validations = (
- ApcValidationAnnee.query.filter_by(
- etudid=self.etud.id,
- ordre=self.annee_but,
- )
- .join(Formation)
- .filter_by(formation_code=self.formsemestre.formation.formation_code)
+ validations = ApcValidationAnnee.query.filter_by(
+ etudid=self.etud.id,
+ ordre=self.annee_but,
+ referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
)
for validation in validations:
db.session.delete(validation)
@@ -935,9 +965,17 @@ class DecisionsProposeesAnnee(DecisionsProposees):
dec_ue = self.decisions_ues.get(ue.id)
if dec_ue:
if dec_ue.code_valide not in CODES_UE_VALIDES:
- messages.append(
- f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
- )
+ if (
+ dec_ue.ue_status
+ and dec_ue.ue_status["was_capitalized"]
+ ):
+ messages.append(
+ f"Information: l'UE {ue.acronyme} capitalisée est utilisée pour un RCUE cette année"
+ )
+ else:
+ messages.append(
+ f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
+ )
else:
messages.append(
f"L'UE {ue.acronyme} n'a pas décision (???)"
@@ -1207,6 +1245,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
ue1_id=ue1.id,
ue2_id=ue2.id,
code=sco_codes.ADSUP,
+ formsemestre_id=self.deca.formsemestre.id, # origine
)
db.session.add(validation_rcue)
db.session.commit()
@@ -1233,13 +1272,16 @@ class DecisionsProposeesRCUE(DecisionsProposees):
self, semestre_id: int, ordre_inferieur: int, competence: ApcCompetence
):
"""Au besoin, enregistre une validation d'UE ADSUP pour le niveau de compétence
- semestre_id : l'indice du semestre concerné (le pair ou l'impair)
+ semestre_id : l'indice du semestre concerné (le pair ou l'impair du niveau courant)
"""
- # Les validations d'UE impaires existantes pour ce niveau inférieur ?
+ semestre_id_inferieur = semestre_id - 2
+ if semestre_id_inferieur < 1:
+ return
+ # Les validations d'UE existantes pour ce niveau inférieur ?
validations_ues: list[ScolarFormSemestreValidation] = (
ScolarFormSemestreValidation.query.filter_by(etudid=self.etud.id)
.join(UniteEns)
- .filter_by(semestre_idx=semestre_id)
+ .filter_by(semestre_idx=semestre_id_inferieur)
.join(ApcNiveau)
.filter_by(ordre=ordre_inferieur)
.join(ApcCompetence)
@@ -1254,13 +1296,14 @@ class DecisionsProposeesRCUE(DecisionsProposees):
# Il faut créer une validation d'UE
# cherche l'UE de notre formation associée à ce niveau
# et warning si il n'y en a pas
- ue = self._get_ue_inferieure(semestre_id, ordre_inferieur, competence)
+ ue = self._get_ue_inferieure(
+ semestre_id_inferieur, ordre_inferieur, competence
+ )
if not ue:
# programme incomplet ou mal paramétré
flash(
- f"""Impossible de valider l'UE inférieure du niveau {
- ordre_inferieur
- } de la compétence {competence.titre}
+ f"""Impossible de valider l'UE inférieure de la compétence {
+ competence.titre} (niveau {ordre_inferieur})
car elle n'existe pas dans la formation
""",
"warning",
@@ -1287,15 +1330,11 @@ class DecisionsProposeesRCUE(DecisionsProposees):
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,
- )
- .join(Formation)
- .filter_by(formation_code=self.deca.formsemestre.formation.formation_code)
- .all()
- )
+ validations_annee: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
+ etudid=self.etud.id,
+ ordre=annee_inferieure,
+ referentiel_competence_id=self.deca.formsemestre.formation.referentiel_competence_id,
+ ).all()
if len(validations_annee) > 1:
log(
f"warning: {len(validations_annee)} validations d'année\n{validations_annee}"
@@ -1332,8 +1371,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
validation_annee = ApcValidationAnnee(
etudid=self.etud.id,
ordre=annee_inferieure,
+ referentiel_competence_id=self.deca.formsemestre.formation.referentiel_competence_id,
code=sco_codes.ADSUP,
- formation_id=self.deca.formsemestre.formation_id,
# met cette validation sur l'année scolaire actuelle, pas la précédente
annee_scolaire=self.deca.formsemestre.annee_scolaire(),
)
@@ -1575,16 +1614,11 @@ class DecisionsProposeesUE(DecisionsProposees):
# 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=ordre,
-# )
-# .join(Formation)
-# .filter(
-# Formation.formation_code == self.formsemestre.formation.formation_code
+# referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id
# )
# .count()
# > 0
diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py
index 29630af0..d0933c7a 100644
--- a/app/but/jury_but_pv.py
+++ b/app/but/jury_but_pv.py
@@ -12,6 +12,7 @@ from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from app import log
from app.but import jury_but
+from app.but.cursus_but import but_ects_valides
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.scodoc.gen_tables import GenTable
@@ -109,6 +110,11 @@ def pvjury_table_but(
"""
# remplace pour le BUT la fonction sco_pv_forms.pvjury_table
annee_but = (formsemestre.semestre_id + 1) // 2
+ referentiel_competence_id = formsemestre.formation.referentiel_competence_id
+ if referentiel_competence_id is None:
+ raise ScoValueError(
+ "pas de référentiel de compétences associé à la formation de ce semestre !"
+ )
titles = {
"nom": "Code" if anonymous else "Nom",
"cursus": "Cursus",
@@ -153,7 +159,7 @@ def pvjury_table_but(
etudid=etud.id,
),
"cursus": _descr_cursus_but(etud),
- "ects": f"{deca.ects_annee():g}",
+ "ects": f"""{deca.ects_annee():g}
Tot. {but_ects_valides(etud, referentiel_competence_id ):g}""",
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
if deca
diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py
index c9c9348b..d3e8db03 100644
--- a/app/comp/bonus_spo.py
+++ b/app/comp/bonus_spo.py
@@ -194,7 +194,7 @@ class BonusSportAdditif(BonusSport):
# les points au dessus du seuil sont comptés (defaut: seuil_moy_gen):
seuil_comptage = None
proportion_point = 0.05 # multiplie les points au dessus du seuil
- bonux_max = 20.0 # le bonus ne peut dépasser 20 points
+ bonus_max = 20.0 # le bonus ne peut dépasser 20 points
bonus_min = 0.0 # et ne peut pas être négatif
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
@@ -435,8 +435,11 @@ class BonusAmiens(BonusSportAdditif):
class BonusBesanconVesoul(BonusSportAdditif):
"""Bonus IUT Besançon - Vesoul pour les UE libres
-
Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,2 point
- sur toutes les moyennes d'UE.
+
Le bonus est compris entre 0 et 0,2 points.
+ et est reporté sur les moyennes d'UE.
+
+ La valeur saisie doit être entre 0 et 0,2: toute valeur
+ supérieure à 0,2 entraine un bonus de 0,2.
"""
@@ -444,7 +447,7 @@ class BonusBesanconVesoul(BonusSportAdditif):
displayed_name = "IUT de Besançon - Vesoul"
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
seuil_moy_gen = 0.0 # tous les points sont comptés
- proportion_point = 1e10 # infini
+ proportion_point = 1
bonus_max = 0.2
@@ -1057,6 +1060,36 @@ class BonusLyon(BonusSportAdditif):
)
+class BonusLyon3(BonusSportAdditif):
+ """IUT de Lyon 3 (septembre 2022)
+
+ Nous avons deux types de bonifications : sport et/ou culture
+
+
+ Pour chaque point au-dessus de 10 obtenu en sport ou en culture nous
+ ajoutons 0,03 points à toutes les moyennes d’UE du semestre. Exemple : 16 en
+ sport ajoute 6*0,03 = 0,18 points à toutes les moyennes d’UE du semestre.
+
+
+ Les bonifications sport et culture peuvent se cumuler dans la limite de 0,3
+ points ajoutés aux moyennes des UE. Exemple : 17 en sport et 16 en culture
+ conduisent au calcul (7 + 6)*0,03 = 0,39 qui dépasse 0,3. La bonification
+ dans ce cas ne sera que de 0,3 points ajoutés à toutes les moyennes d’UE du
+ semestre.
+
+
+ Dans Scodoc on déclarera une UE Sport&Culture dans laquelle on aura un
+ module pour le Sport et un autre pour la Culture avec pour chaque module la
+ note sur 20 obtenue en sport ou en culture par l’étudiant.
+
+ """
+
+ name = "bonus_lyon3"
+ displayed_name = "IUT de Lyon 3"
+ proportion_point = 0.03
+ bonus_max = 0.3
+
+
class BonusMantes(BonusSportAdditif):
"""Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines.
diff --git a/app/comp/jury.py b/app/comp/jury.py
index 1c43158d..ee2c1373 100644
--- a/app/comp/jury.py
+++ b/app/comp/jury.py
@@ -231,12 +231,11 @@ def erase_decisions_annee_formation(
.all()
)
# Année BUT
- validations += (
- ApcValidationAnnee.query.filter_by(etudid=etud.id, ordre=annee)
- .join(Formation)
- .filter_by(formation_code=formation.formation_code)
- .all()
- )
+ validations += ApcValidationAnnee.query.filter_by(
+ etudid=etud.id,
+ ordre=annee,
+ referentiel_competence_id=formation.referentiel_competence_id,
+ ).all()
# Autorisations vers les semestres suivants ceux de l'année:
validations += (
ScolarAutorisationInscription.query.filter_by(
diff --git a/app/comp/res_but.py b/app/comp/res_but.py
index 163d56f1..b2ba3eb5 100644
--- a/app/comp/res_but.py
+++ b/app/comp/res_but.py
@@ -337,17 +337,15 @@ class ResultatsSemestreBUT(NotesTableCompat):
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,
- ),
- )
+ validations = ApcValidationAnnee.query.filter_by(
+ ordre=annee_but,
+ referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
+ ).join(
+ FormSemestreInscription,
+ db.and_(
+ FormSemestreInscription.etudid == ApcValidationAnnee.etudid,
+ FormSemestreInscription.formsemestre_id == self.formsemestre.id,
+ ),
)
validation_by_etud = {}
for validation in validations:
diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py
index 824e7e3b..b33e2f2b 100644
--- a/app/models/but_refcomp.py
+++ b/app/models/but_refcomp.py
@@ -94,6 +94,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
backref="referentiel_competence",
order_by="Formation.acronyme, Formation.version",
)
+ validations_annee = db.relationship(
+ "ApcValidationAnnee",
+ backref="referentiel_competence",
+ lazy="dynamic",
+ )
def __repr__(self):
return f""
@@ -359,6 +364,9 @@ class ApcNiveau(db.Model, XMLModel):
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>"""
+ def __str__(self):
+ return f"""{self.competence.titre} niveau {self.ordre}"""
+
def to_dict(self, with_app_critiques=True):
"as a dict, recursif (ou non) sur les AC"
return {
diff --git a/app/models/but_validations.py b/app/models/but_validations.py
index 997d1a46..185ab539 100644
--- a/app/models/but_validations.py
+++ b/app/models/but_validations.py
@@ -2,8 +2,6 @@
"""Décisions de jury (validations) des RCUE et années du BUT
"""
-from typing import Union
-
from app import db
from app.models import CODE_STR_LEN
@@ -38,7 +36,7 @@ class ApcValidationRCUE(db.Model):
formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
)
- "formsemestre pair du RCUE"
+ "formsemestre origine du RCUE (celui d'où a été émis la validation)"
# 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)
@@ -106,73 +104,14 @@ class ApcValidationRCUE(db.Model):
}
-# unused
-# def find_rcues(
-# 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.
-
-# Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
-# En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
-
-# Résultat: la liste peut être vide.
-# """
-# if (ue.niveau_competence is None) or (ue.semestre_idx is None):
-# return []
-
-# if ue.semestre_idx % 2: # S1, S3, S5
-# other_semestre_idx = ue.semestre_idx + 1
-# else:
-# other_semestre_idx = ue.semestre_idx - 1
-
-# cursor = db.session.execute(
-# text(
-# """SELECT
-# ue.id, formsemestre.id
-# FROM
-# notes_ue ue,
-# notes_formsemestre_inscription inscr,
-# notes_formsemestre formsemestre
-
-# WHERE
-# inscr.etudid = :etudid
-# AND inscr.formsemestre_id = formsemestre.id
-
-# AND formsemestre.semestre_id = :other_semestre_idx
-# AND ue.formation_id = formsemestre.formation_id
-# AND ue.niveau_competence_id = :ue_niveau_competence_id
-# AND ue.semestre_idx = :other_semestre_idx
-# """
-# ),
-# {
-# "etudid": etud.id,
-# "other_semestre_idx": other_semestre_idx,
-# "ue_niveau_competence_id": ue.niveau_competence_id,
-# },
-# )
-# rcues = []
-# for ue_id, formsemestre_id in cursor:
-# other_ue = UniteEns.query.get(ue_id)
-# other_formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
-# rcues.append(
-# 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
-# return rcues
-
-
class ApcValidationAnnee(db.Model):
"""Validation des années du BUT"""
__tablename__ = "apc_validation_annee"
# Assure unicité de la décision:
__table_args__ = (
- db.UniqueConstraint("etudid", "ordre", "formation_id"),
- ) # il aurait été plus intelligent de mettre ici le refcomp
+ db.UniqueConstraint("etudid", "ordre", "referentiel_competence_id"),
+ )
id = db.Column(db.Integer, primary_key=True)
etudid = db.Column(
db.Integer,
@@ -185,11 +124,9 @@ class ApcValidationAnnee(db.Model):
formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
)
- "le semestre IMPAIR (le 1er) de l'année"
- formation_id = db.Column( # il aurait été plus intelligent de mettre ici le refcomp
- db.Integer,
- db.ForeignKey("notes_formations.id"),
- nullable=False,
+ "le semestre origine, normalement l'IMPAIR (le 1er) de l'année"
+ referentiel_competence_id = db.Column(
+ db.Integer, db.ForeignKey("apc_referentiel_competences.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())
@@ -209,17 +146,30 @@ class ApcValidationAnnee(db.Model):
"dict pour bulletins"
return {
"annee_scolaire": self.annee_scolaire,
- "date": self.date.isoformat(),
+ "date": self.date.isoformat() if self.date else "",
"code": self.code,
"ordre": self.ordre,
}
def html(self) -> str:
"Affichage html"
+ date_str = (
+ f"""le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}"""
+ if self.date
+ else "(sans date)"
+ )
+ link = (
+ self.formsemestre.html_link_status(
+ label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
+ title=self.formsemestre.titre_annee(),
+ )
+ if self.formsemestre
+ else "externe/antérieure"
+ )
return f"""Validation année BUT{self.ordre} émise par
- {self.formsemestre.html_link_status() if self.formsemestre else "-"}
+ {link}
: {self.code}
- le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
+ {date_str}
"""
@@ -261,15 +211,11 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
decisions["descr_decisions_rcue"] = ""
decisions["descr_decisions_niveaux"] = ""
# --- 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(Formation)
- .filter(Formation.formation_code == formsemestre.formation.formation_code)
- .first()
- )
+ validation = ApcValidationAnnee.query.filter_by(
+ etudid=etud.id,
+ annee_scolaire=formsemestre.annee_scolaire(),
+ referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
+ ).first()
if validation:
decisions["decision_annee"] = validation.to_dict_bul()
else:
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 37e09d14..d43bb09b 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -165,12 +165,12 @@ class FormSemestre(db.Model):
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
- def html_link_status(self) -> str:
+ def html_link_status(self, label=None, title=None) -> str:
"html link to status page"
return f"""{self.titre_mois()}
+ }" title="{title or ''}">{label or self.titre_mois()}
"""
@classmethod
@@ -528,6 +528,11 @@ class FormSemestre(db.Model):
return ""
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
+ def add_etape(self, etape_apo: str):
+ "Ajoute une étape"
+ etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo)
+ db.session.add(etape)
+
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
"""Calcule la liste des regroupements cohérents d'UE impliquant ce
formsemestre.
@@ -873,15 +878,12 @@ class FormSemestre(db.Model):
.order_by(UniteEns.numero)
.all()
)
- vals_annee = ( # issues de ce formsemestre seulement
+ vals_annee = ( # issues de cette année scolaire seulement
ApcValidationAnnee.query.filter_by(
etudid=etudid,
annee_scolaire=self.annee_scolaire(),
- )
- .join(ApcValidationAnnee.formsemestre)
- .join(FormSemestre.formation)
- .filter(Formation.formation_code == self.formation.formation_code)
- .all()
+ referentiel_competence_id=self.formation.referentiel_competence_id,
+ ).all()
)
H = []
for vals in (vals_sem, vals_ues, vals_rcues, vals_annee):
diff --git a/app/models/validations.py b/app/models/validations.py
index 9e2cf5e2..d4ca5bb0 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -56,6 +56,7 @@ class ScolarFormSemestreValidation(db.Model):
)
ue = db.relationship("UniteEns", lazy="select", uselist=False)
+ etud = db.relationship("Identite", backref="validations")
formsemestre = db.relationship(
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
)
@@ -94,6 +95,14 @@ class ScolarFormSemestreValidation(db.Model):
if self.moy_ue is not None
else ""
)
+ link = (
+ self.formsemestre.html_link_status(
+ label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
+ title=self.formsemestre.titre_annee(),
+ )
+ if self.formsemestre
+ else "externe/antérieure"
+ )
return f"""Validation
{'externe' if self.is_external else ""}
de l'UE {self.ue.acronyme}
@@ -101,9 +110,7 @@ class ScolarFormSemestreValidation(db.Model):
+ ", ".join([p.code for p in self.ue.parcours]))
+ ""
if self.ue.parcours else ""}
- de {self.ue.formation.acronyme}
- {("émise par " + self.formsemestre.html_link_status())
- if self.formsemestre else "externe/antérieure"}
+ {("émise par " + link)}
: {self.code}{moyenne}
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
@@ -149,10 +156,16 @@ class ScolarAutorisationInscription(db.Model):
def html(self) -> str:
"Affichage html"
+ link = (
+ self.origin_formsemestre.html_link_status(
+ label=f"{self.origin_formsemestre.titre_formation(with_sem_idx=1)}",
+ title=self.origin_formsemestre.titre_annee(),
+ )
+ if self.origin_formsemestre
+ else "externe/antérieure"
+ )
return f"""Autorisation de passage vers S{self.semestre_id} émise par
- {self.origin_formsemestre.html_link_status()
- if self.origin_formsemestre
- else "-"}
+ {link}
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
"""
diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py
index 85d14b95..dff4ead7 100644
--- a/app/scodoc/codes_cursus.py
+++ b/app/scodoc/codes_cursus.py
@@ -196,6 +196,8 @@ CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
CODES_SEM_REO = {NAR} # reorientation
+# Les codes d'UEs
+CODES_JURY_UE = {ADM, CMP, ADJ, ADJR, ADSUP, AJ, ATJ, RAT, DEF, ABAN, DEM, UEBSL}
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
"UE validée"
diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py
index 436ecde2..2b5e867a 100644
--- a/app/scodoc/gen_tables.py
+++ b/app/scodoc/gen_tables.py
@@ -88,7 +88,7 @@ class DEFAULT_TABLE_PREFERENCES(object):
return self.values[k]
-class GenTable(object):
+class GenTable:
"""Simple 2D tables with export to HTML, PDF, Excel, CSV.
Can be sub-classed to generate fancy formats.
"""
@@ -197,6 +197,9 @@ class GenTable(object):
def __repr__(self):
return f""
+ def __len__(self):
+ return len(self.rows)
+
def get_nb_cols(self):
return len(self.columns_ids)
diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
index 1348cbe8..61af26bd 100644
--- a/app/scodoc/sco_apogee_csv.py
+++ b/app/scodoc/sco_apogee_csv.py
@@ -51,7 +51,14 @@ from app import log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.comp.res_but import ResultatsSemestreBUT
-from app.models import FormSemestre, Identite, ApcValidationAnnee
+from app.models import (
+ ApcValidationAnnee,
+ ApcValidationRCUE,
+ FormSemestre,
+ Identite,
+ ScolarFormSemestreValidation,
+)
+
from app.models.config import ScoDocSiteConfig
from app.scodoc.sco_apogee_reader import (
APO_DECIMAL_SEP,
@@ -64,6 +71,7 @@ from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.codes_cursus import code_semestre_validant
from app.scodoc.codes_cursus import (
+ ADSUP,
DEF,
DEM,
NAR,
@@ -216,7 +224,12 @@ class ApoEtud(dict):
break
self.col_elts[code] = elt
if elt is None:
- self.new_cols[col_id] = self.cols[col_id]
+ try:
+ self.new_cols[col_id] = self.cols[col_id]
+ except KeyError as exc:
+ raise ScoFormatError(
+ f"""Fichier Apogee invalide : ligne mal formatée ?
colonne {col_id} non déclarée ?"""
+ ) from exc
else:
try:
self.new_cols[col_id] = sco_elts[code][
@@ -323,14 +336,22 @@ class ApoEtud(dict):
x.strip() for x in ue["code_apogee"].split(",")
}:
if self.export_res_ues:
- if decisions_ue and ue["ue_id"] in decisions_ue:
+ if (
+ decisions_ue and ue["ue_id"] in decisions_ue
+ ) or self.export_res_sdj:
ue_status = res.get_etud_ue_status(etudid, ue["ue_id"])
- code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
+ if decisions_ue and ue["ue_id"] in decisions_ue:
+ code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
+ code_decision_ue_apo = ScoDocSiteConfig.get_code_apo(
+ code_decision_ue
+ )
+ else:
+ code_decision_ue_apo = ""
return dict(
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
B=20,
J="",
- R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
+ R=code_decision_ue_apo,
M="",
)
else:
@@ -343,14 +364,17 @@ class ApoEtud(dict):
module_code_found = False
for modimpl in modimpls:
module = modimpl["module"]
- if module["code_apogee"] and code in {
- x.strip() for x in module["code_apogee"].split(",")
- }:
+ if (
+ res.modimpl_inscr_df[modimpl["moduleimpl_id"]][etudid]
+ and module["code_apogee"]
+ and code in {x.strip() for x in module["code_apogee"].split(",")}
+ ):
n = res.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
if n != "NI" and self.export_res_modules:
return dict(N=self.fmt_note(n), B=20, J="", R="")
else:
module_code_found = True
+
if module_code_found:
return VOID_APO_RES
#
@@ -491,15 +515,11 @@ class ApoEtud(dict):
# ne trouve pas de semestre impair
self.validation_annee_but = None
return
- self.validation_annee_but: ApcValidationAnnee = (
- ApcValidationAnnee.query.filter_by(
- formsemestre_id=formsemestre.id,
- etudid=self.etud["etudid"],
- formation_id=self.cur_sem[
- "formation_id"
- ], # XXX utiliser formation_code
- ).first()
- )
+ self.validation_annee_but: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
+ formsemestre_id=formsemestre.id,
+ etudid=self.etud["etudid"],
+ referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
+ ).first()
self.is_nar = (
self.validation_annee_but and self.validation_annee_but.code == NAR
)
@@ -899,6 +919,75 @@ class ApoData:
)
return T
+ def build_adsup_table(self):
+ """Construit une table listant les ADSUP émis depuis les formsemestres
+ NIP nom prenom nom_formsemestre etape UE
+ """
+ validations_ues, validations_rcue = self.list_adsup()
+ rows = [
+ {
+ "code_nip": v.etud.code_nip,
+ "nom": v.etud.nom,
+ "prenom": v.etud.prenom,
+ "formsemestre": v.formsemestre.titre_formation(with_sem_idx=1),
+ "etape": v.formsemestre.etapes_apo_str(),
+ "ue": v.ue.acronyme,
+ }
+ for v in validations_ues
+ ]
+ rows += [
+ {
+ "code_nip": v.etud.code_nip,
+ "nom": v.etud.nom,
+ "prenom": v.etud.prenom,
+ "formsemestre": v.formsemestre.titre_formation(with_sem_idx=1),
+ "etape": "", # on ne sait pas à quel étape rattacher le RCUE
+ "rcue": f"{v.ue1.acronyme}/{v.ue2.acronyme}",
+ }
+ for v in validations_rcue
+ ]
+
+ return GenTable(
+ columns_ids=(
+ "code_nip",
+ "nom",
+ "prenom",
+ "formsemestre",
+ "etape",
+ "ue",
+ "rcue",
+ ),
+ titles={
+ "code_nip": "NIP",
+ "nom": "Nom",
+ "prenom": "Prénom",
+ "formsemestre": "Semestre",
+ "etape": "Etape",
+ "ue": "UE",
+ "rcue": "RCUE",
+ },
+ rows=rows,
+ xls_sheet_name="ADSUPs",
+ )
+
+ def list_adsup(
+ self,
+ ) -> tuple[list[ScolarFormSemestreValidation], list[ApcValidationRCUE]]:
+ """Liste les validations ADSUP émises par des formsemestres de cet ensemble"""
+ validations_ues = (
+ ScolarFormSemestreValidation.query.filter_by(code=ADSUP)
+ .filter(ScolarFormSemestreValidation.ue_id != None)
+ .filter(
+ ScolarFormSemestreValidation.formsemestre_id.in_(
+ self.etape_formsemestre_ids
+ )
+ )
+ )
+ validations_rcue = ApcValidationRCUE.query.filter_by(code=ADSUP).filter(
+ ApcValidationRCUE.formsemestre_id.in_(self.etape_formsemestre_ids)
+ )
+ return validations_ues, validations_rcue
+
def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
"""
@@ -1025,6 +1114,10 @@ def export_csv_to_apogee(
cr_table = apo_data.build_cr_table()
cr_xls = cr_table.excel()
+ # ADSUPs
+ adsup_table = apo_data.build_adsup_table()
+ adsup_xls = adsup_table.excel() if len(adsup_table) else None
+
# Create ZIP
if not dest_zip:
data = io.BytesIO()
@@ -1050,6 +1143,7 @@ def export_csv_to_apogee(
log_filename = "scodoc-" + basename + ".log.txt"
nar_filename = basename + "-nar" + scu.XLSX_SUFFIX
cr_filename = basename + "-decisions" + scu.XLSX_SUFFIX
+ adsup_filename = f"{basename}-adsups{scu.XLSX_SUFFIX}"
logf = io.StringIO()
logf.write(f"export_to_apogee du {time.ctime()}\n\n")
@@ -1086,6 +1180,8 @@ def export_csv_to_apogee(
"\n\nElements Apogee inconnus dans ces semestres ScoDoc:\n"
+ "\n".join(apo_data.list_unknown_elements())
)
+ if adsup_xls:
+ logf.write(f"\n\nADSUP générés: {len(adsup_table)}\n")
log(logf.getvalue()) # sortie aussi sur le log ScoDoc
# Write data to ZIP
@@ -1094,6 +1190,8 @@ def export_csv_to_apogee(
if nar_xls:
dest_zip.writestr(nar_filename, nar_xls)
dest_zip.writestr(cr_filename, cr_xls)
+ if adsup_xls:
+ dest_zip.writestr(adsup_filename, adsup_xls)
if my_zip:
dest_zip.close()
diff --git a/app/scodoc/sco_apogee_reader.py b/app/scodoc/sco_apogee_reader.py
index 1e798433..2a56d85d 100644
--- a/app/scodoc/sco_apogee_reader.py
+++ b/app/scodoc/sco_apogee_reader.py
@@ -295,8 +295,15 @@ class ApoCSVReadWrite:
filename=self.get_filename(),
)
cols = {} # { col_id : value }
- for i, field in enumerate(fields):
- cols[self.col_ids[i]] = field
+ try:
+ for i, field in enumerate(fields):
+ cols[self.col_ids[i]] = field
+ except IndexError as exc:
+ raise
+ raise ScoFormatError(
+ f"Fichier Apogee incorrect (colonnes excédentaires ? ({i}/{field}))",
+ filename=self.get_filename(),
+ ) from exc
etud_tuples.append(
ApoEtudTuple(
nip=fields[0], # id etudiant
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index f145b3f2..23109447 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -398,7 +398,7 @@ def formsemestre_validation_etud(
selected_choice = choice
break
if not selected_choice:
- raise ValueError("code choix invalide ! (%s)" % codechoice)
+ raise ValueError(f"code choix invalide ! ({codechoice})")
#
Se.valide_decision(selected_choice) # enregistre
return _redirect_valid_choice(
@@ -1132,6 +1132,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
},
)
)
+ ue_codes = sorted(codes_cursus.CODES_JURY_UE)
form_descr += [
(
"date",
@@ -1152,6 +1153,18 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
"title": "Moyenne (/20) obtenue dans cette UE:",
},
),
+ (
+ "code_jury",
+ {
+ "input_type": "menu",
+ "title": "Code jury",
+ "explanation": " code donné par le jury (ADM si validée normalement)",
+ "allow_null": True,
+ "allowed_values": [""] + ue_codes,
+ "labels": ["-"] + ue_codes,
+ "default": ADM,
+ },
+ ),
]
tf = TrivialFormulator(
request.base_url,
@@ -1173,17 +1186,20 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
de {etud.html_link_fiche()}
- Utiliser cette page pour enregistrer une UE validée antérieurement,
+
Utiliser cette page pour enregistrer des UEs validées antérieurement,
dans un semestre hors ScoDoc.
- Les UE validées dans ScoDoc sont déjà
- automatiquement prises en compte. Cette page n'est utile que pour les étudiants ayant
- suivi un début de cursus dans un autre établissement, ou bien dans un semestre géré
- sans ScoDoc et qui redouble ce semestre
- (pour les semestres précédents gérés avec ScoDoc,
- passer par la page jury normale)).
+
Les UE validées dans ScoDoc sont
+ automatiquement prises en compte.
+
+ Cette page est surtout utile pour les étudiants ayant
+ suivi un début de cursus dans un autre établissement, ou qui
+ ont suivi une UE à l'étranger ou dans un semestre géré sans ScoDoc.
+
+ Pour les semestres précédents gérés avec ScoDoc, passer par la page jury normale.
+
+ Notez que l'UE est validée, avec enregistrement immédiat de la décision et
+ l'attribution des ECTS si le code jury est validant (ADM).
- Notez que l'UE est validée (ADM), avec enregistrement immédiat de la décision et
- l'attribution des ECTS.
On ne peut valider ici que les UEs du cursus {formation.titre}
{_get_etud_ue_cap_html(etud, formsemestre)}
@@ -1221,12 +1237,16 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
else:
semestre_id = None
+ if tf[2]["code_jury"] not in CODES_JURY_UE:
+ flash("Code UE invalide")
+ return flask.redirect(dest_url)
do_formsemestre_validate_previous_ue(
formsemestre,
etud.id,
tf[2]["ue_id"],
tf[2]["moy_ue"],
tf[2]["date"],
+ code=tf[2]["code_jury"],
semestre_id=semestre_id,
)
flash("Validation d'UE enregistrée")
@@ -1258,7 +1278,7 @@ def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
Liste de toutes les UEs validées par {etud.html_link_fiche()},
sur des semestres ou déclarées comme "antérieures" (externes).
- """
+ """
]
for validation in validations:
if validation.formsemestre_id is None:
@@ -1267,17 +1287,20 @@ def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
origine = f", du semestre {formsemestre.html_link_status()}"
if validation.semestre_id is not None:
origine += f" (S{validation.semestre_id})"
- H.append(
- f"""
- - {validation.html()}
+ H.append(f"""
- {validation.html()}""")
+ if validation.formsemestre.can_edit_jury():
+ H.append(
+ f"""
-
- """,
- )
+ """,
+ )
+ else:
+ H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé"))
+ H.append("")
H.append("
")
return "\n".join(H)
@@ -1300,7 +1323,7 @@ def do_formsemestre_validate_previous_ue(
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
cnx = ndb.GetDBConnexion()
- if ue_coefficient != None:
+ if ue_coefficient is not None:
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
cnx, formsemestre.id, ue_id, ue_coefficient
)
diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index e2dbd1cb..cbb26f08 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -59,11 +59,13 @@ from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_t
from app.scodoc.sco_permissions import Permission
-def _menu_scolarite(authuser, sem: dict, etudid: int):
+def _menu_scolarite(
+ authuser, formsemestre: FormSemestre, etudid: int, etat_inscription: str
+):
"""HTML pour menu "scolarite" pour un etudiant dans un semestre.
Le contenu du menu depend des droits de l'utilisateur et de l'état de l'étudiant.
"""
- locked = not sem["etat"]
+ locked = not formsemestre.etat
if locked:
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
return lockicon # no menu
@@ -71,10 +73,10 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
Permission.ScoEtudInscrit
) and not authuser.has_permission(Permission.ScoEtudChangeGroups):
return "" # no menu
- ins = sem["ins"]
- args = {"etudid": etudid, "formsemestre_id": ins["formsemestre_id"]}
- if ins["etat"] != "D":
+ args = {"etudid": etudid, "formsemestre_id": formsemestre.id}
+
+ if etat_inscription != scu.DEMISSION:
dem_title = "Démission"
dem_url = "scolar.form_dem"
else:
@@ -82,14 +84,14 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
dem_url = "scolar.do_cancel_dem"
# Note: seul un etudiant inscrit (I) peut devenir défaillant.
- if ins["etat"] != codes_cursus.DEF:
+ if etat_inscription != codes_cursus.DEF:
def_title = "Déclarer défaillance"
def_url = "scolar.form_def"
- elif ins["etat"] == codes_cursus.DEF:
+ elif etat_inscription == codes_cursus.DEF:
def_title = "Annuler la défaillance"
def_url = "scolar.do_cancel_def"
def_enabled = (
- (ins["etat"] != "D")
+ (etat_inscription != scu.DEMISSION)
and authuser.has_permission(Permission.ScoEtudInscrit)
and not locked
)
@@ -128,6 +130,12 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
"enabled": authuser.has_permission(Permission.ScoEtudInscrit)
and not locked,
},
+ {
+ "title": "Gérer les validations d'UEs antérieures",
+ "endpoint": "notes.formsemestre_validate_previous_ue",
+ "args": args,
+ "enabled": formsemestre.can_edit_jury(),
+ },
{
"title": "Inscrire à un autre semestre",
"endpoint": "notes.formsemestre_inscription_with_modules_form",
@@ -250,8 +258,8 @@ def ficheEtud(etudid=None):
info["last_formsemestre_id"] = ""
sem_info = {}
for sem in info["sems"]:
+ formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
if sem["ins"]["etat"] != scu.INSCRIT:
- formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
descr, _ = etud_descr_situation_semestre(
etudid,
formsemestre,
@@ -283,7 +291,7 @@ def ficheEtud(etudid=None):
)
grlink = ", ".join(grlinks)
# infos ajoutées au semestre dans le parcours (groupe, menu)
- menu = _menu_scolarite(authuser, sem, etudid)
+ menu = _menu_scolarite(authuser, formsemestre, etudid, sem["ins"]["etat"])
if menu:
sem_info[sem["formsemestre_id"]] = (
"" + grlink + " | " + menu + " |
"
diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py
index c0284293..f3d8c6d3 100644
--- a/app/scodoc/sco_semset.py
+++ b/app/scodoc/sco_semset.py
@@ -42,6 +42,7 @@ sem_set_list()
import flask
from flask import g, url_for
+from app import log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
@@ -52,7 +53,6 @@ from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
-from app import log
from app.scodoc.sco_etape_bilan import EtapeBilan
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_vdi import ApoEtapeVDI
diff --git a/app/static/css/jury_delete_manual.css b/app/static/css/jury_delete_manual.css
index d41e4f80..b685384a 100644
--- a/app/static/css/jury_delete_manual.css
+++ b/app/static/css/jury_delete_manual.css
@@ -1,9 +1,12 @@
-
div.jury_decisions_list div {
font-size: 120%;
font-weight: bold;
}
span.parcours {
- color:blueviolet;
+ color: blueviolet;
}
+
+div.ue_list_etud_validations ul.liste_validations li {
+ margin-bottom: 8px;
+}
\ No newline at end of file
diff --git a/app/static/js/validate_previous_ue.js b/app/static/js/validate_previous_ue.js
index 22655f49..c2fe3c88 100644
--- a/app/static/js/validate_previous_ue.js
+++ b/app/static/js/validate_previous_ue.js
@@ -11,27 +11,30 @@ document.addEventListener("DOMContentLoaded", () => {
// Handle button click event here
event.preventDefault();
const etudid = event.target.dataset.etudid;
- const v_id = event.target.dataset.v_id;
+ const validation_id = event.target.dataset.v_id;
const validation_type = event.target.dataset.type;
if (confirm("Supprimer cette validation ?")) {
- fetch(
- `${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`,
- {
- method: "POST",
- }
- ).then((response) => {
- // Handle the response
- if (response.ok) {
- location.reload();
- } else {
- throw new Error("Request failed");
- }
- });
+ delete_validation(etudid, validation_type, validation_id);
}
});
});
});
+async function delete_validation(etudid, validation_type, validation_id) {
+ const response = await fetch(
+ `${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${validation_id}/delete`,
+ {
+ method: "POST",
+ }
+ );
+ if (response.ok) {
+ location.reload();
+ } else {
+ const data = await response.json();
+ sco_error_message("erreur: " + data.message);
+ }
+}
+
function update_ue_list() {
var ue_id = $("#tf_ue_id")[0].value;
if (ue_id) {
diff --git a/app/templates/but/formsemestre_validation_auto_but.j2 b/app/templates/but/formsemestre_validation_auto_but.j2
index 5bf69b39..0f8e827b 100644
--- a/app/templates/but/formsemestre_validation_auto_but.j2
+++ b/app/templates/but/formsemestre_validation_auto_but.j2
@@ -9,38 +9,41 @@
{% block app_content %}
-
Calcul automatique des décisions de jury du BUT
-
- - N'enregistre jamais de décisions de l'année scolaire précédente, même
- si on a des RCUE "à cheval" sur deux années.
-
-
- - Attention: peut modifier des décisions déjà enregistrées, si la
- validation de droit est calculée. Par exemple, vous aviez saisi RAT
- pour un étudiant dont les moyennes d'UE dépassent 10 mais qui pour une
- raison particulière ne valide pas son année. Le calcul automatique peut
- remplacer ce RAT par un ADM, ScoDoc considérant que les
- conditions sont satisfaites. On peut éviter cela en laissant une note de
- l'étudiant en ATTente.
-
-
- - N'enregistre que les décisions validantes de droit: ADM ou CMP.
-
- - N'enregistre pas de décision si l'étudiant a une ou plusieurs notes en ATTente.
-
- - L'assiduité n'est pas prise en compte.
-
-
- En conséquence, saisir ensuite manuellement les décisions manquantes,
- notamment sur les UEs en dessous de 10.
-
-
+
Calcul automatique des décisions de jury du BUT
- - Ne jamais lancer ce calcul avant que toutes les notes ne soient saisies !
- (verrouiller le semestre ensuite)
-
- - Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
-
+
- N'enregistre jamais de décisions de l'année scolaire précédente, même
+ si on a des RCUE "à cheval" sur deux années.
+
+
+
- Attention: peut modifier des décisions déjà enregistrées, si la
+ validation de droit est calculée.
+ Ce calcul n'utilise que les notes, et pas les décisions manuelles déjà saisies.
+
+ Par exemple, vous aviez saisi ATJ ou RAT
+ pour un étudiant dont les moyennes d'UE dépassent 10 mais qui pour une
+ raison particulière ne valide pas son année. Le calcul automatique peut
+ remplacer ce RAT par un ADM, ScoDoc considérant que les
+ conditions sont satisfaites. On peut éviter cela en laissant une note de
+ l'étudiant en ATTente.
+
+
+
- N'enregistre que les décisions validantes de droit: ADM ou CMP.
+
+
- N'enregistre pas de décision si l'étudiant a une ou plusieurs notes en ATTente.
+
+
- L'assiduité n'est pas prise en compte.
+
+
+ En conséquence, saisir ensuite manuellement les décisions manquantes,
+ notamment sur les UEs en dessous de 10.
+
+
+
+ - Ne jamais lancer ce calcul avant que toutes les notes ne soient saisies !
+ (verrouiller le semestre ensuite)
+
+ - Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
+
diff --git a/app/templates/jury/erase_decisions_annee_formation.j2 b/app/templates/jury/erase_decisions_annee_formation.j2
index 0c231b53..4549688b 100644
--- a/app/templates/jury/erase_decisions_annee_formation.j2
+++ b/app/templates/jury/erase_decisions_annee_formation.j2
@@ -4,8 +4,8 @@
{% if not validations %}
Aucune validation de jury enregistrée pour {{etud.html_link_fiche()|safe}}
-sur l'année {{annee}}
-de la formation {{ formation.html() }}
+ sur l'année {{annee}}
+ de la formation {{ formation.html() }}
@@ -16,7 +16,7 @@ de la formation
{{ formation.html() }}
Effacer les décisions de jury pour l'année {{annee}} de {{etud.html_link_fiche()|safe}} ?
Affectera toutes les décisions concernant l'année {{annee}} de la formation,
-quelle que soit leur origine.
+ quelle que soit leur origine.
Les décisions concernées sont:
@@ -34,8 +34,34 @@ quelle que soit leur origine.
{% endif %}
+
{% endif %}
+
+
{% endblock %}
\ No newline at end of file
diff --git a/app/views/notes.py b/app/views/notes.py
index d6a77c61..4bc79eba 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -2534,21 +2534,20 @@ def formsemestre_validation_but(
"""
)
else:
- erase_span = f"""effacer décisions de ce jury
-
+ erase_span = f"""
effacer toutes ses décisions de BUT{deca.annee_but}
+ etudid=deca.etud.id, annee=deca.annee_but, formsemestre_id=formsemestre_id)}"
+ >effacer des décisions de jury
+
+ enregistrer des UEs antérieures
"""
H.append(
f"""
@@ -2966,6 +2965,12 @@ def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int):
)
)
validations = jury.erase_decisions_annee_formation(etud, formation, annee)
+ formsemestre_origine_id = request.args.get("formsemestre_id")
+ formsemestre_origine = (
+ FormSemestre.query.get_or_404(formsemestre_origine_id)
+ if formsemestre_origine_id
+ else None
+ )
return render_template(
"jury/erase_decisions_annee_formation.j2",
annee=annee,
@@ -2974,6 +2979,7 @@ def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int):
),
etud=etud,
formation=formation,
+ formsemestre_origine=formsemestre_origine,
validations=validations,
sco=ScoData(),
title=f"Effacer décisions de jury {etud.nom} - année {annee}",
diff --git a/migrations/versions/829683efddc4_change_apcvalidationannee.py b/migrations/versions/829683efddc4_change_apcvalidationannee.py
index 8955a1a3..79152d57 100644
--- a/migrations/versions/829683efddc4_change_apcvalidationannee.py
+++ b/migrations/versions/829683efddc4_change_apcvalidationannee.py
@@ -7,7 +7,7 @@ Create Date: 2023-06-28 09:47:16.591028
"""
from alembic import op
import sqlalchemy as sa
-
+from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic.
revision = "829683efddc4"
@@ -15,30 +15,96 @@ down_revision = "c701224fa255"
branch_labels = None
depends_on = None
+Session = sessionmaker()
+
+
+# Voir https://stackoverflow.com/questions/24082542/check-if-a-table-column-exists-in-the-database-using-sqlalchemy-and-alembic
+from sqlalchemy import inspect
+
+
+def column_exists(table_name, column_name):
+ bind = op.get_context().bind
+ insp = inspect(bind)
+ columns = insp.get_columns(table_name)
+ return any(c["name"] == column_name for c in columns)
+
def upgrade():
- # ### commands auto generated by Alembic - please adjust! ###
+ if column_exists("apc_validation_annee", "referentiel_competence_id"):
+ return # utile durant developpement
+ # Enleve la contrainte erronée
with op.batch_alter_table("apc_validation_annee", schema=None) as batch_op:
batch_op.drop_constraint(
"apc_validation_annee_etudid_annee_scolaire_ordre_key", type_="unique"
)
- # batch_op.create_unique_constraint(
- # "apc_validation_annee_etudid_formation_ordre_key",
- # ["etudid", "ordre", "formation_id"],
- # )
+ # Ajoute colonne referentiel, nullable pour l'instant
+ batch_op.add_column(
+ sa.Column("referentiel_competence_id", sa.Integer(), nullable=True)
+ )
- # ### end Alembic commands ###
+ # Affecte le referentiel des anciennes validations
+ bind = op.get_bind()
+ session = Session(bind=bind)
+ session.execute(
+ sa.text(
+ """
+ UPDATE apc_validation_annee AS a
+ SET referentiel_competence_id = (
+ SELECT f.referentiel_competence_id
+ FROM notes_formations f
+ WHERE f.id = a.formation_id
+ )
+ """
+ )
+ )
+ # En principe, on n'a pas pu entrer de validation sur des formations sans referentiel
+ # par prudence, on les supprime avant d'ajouter la contrainte
+ session.execute(
+ sa.text(
+ "DELETE FROM apc_validation_annee WHERE referentiel_competence_id is NULL"
+ )
+ )
+ op.alter_column(
+ "apc_validation_annee",
+ "referentiel_competence_id",
+ nullable=False,
+ )
+ op.create_foreign_key(
+ "apc_validation_annee_refcomp_fkey",
+ "apc_validation_annee",
+ "apc_referentiel_competences",
+ ["referentiel_competence_id"],
+ ["id"],
+ )
+ # Efface les validations d'année dupliquées
+ # (garde la validation la plus récente)
+ session.execute(
+ sa.text(
+ """
+ DELETE FROM apc_validation_annee t1
+ WHERE t1.id <> (SELECT max(t2.id)
+ FROM apc_validation_annee t2
+ WHERE t1.etudid = t2.etudid
+ AND t1.referentiel_competence_id = t2.referentiel_competence_id
+ AND t1.ordre = t2.ordre
+ )
+ """
+ )
+ )
+ # Et ajoute la contrainte unicité de décision année par étudiant/ref. comp.:
+ op.create_unique_constraint(
+ "apc_validation_annee_etudid_ordre_refcomp_key",
+ "apc_validation_annee",
+ ["etudid", "ordre", "referentiel_competence_id"],
+ )
+ op.drop_column("apc_validation_annee", "formation_id")
def downgrade():
- # ### commands auto generated by Alembic - please adjust! ###
+ # Se contente de ré-ajouter la colonne formation_id sans re-générer son contenu
with op.batch_alter_table("apc_validation_annee", schema=None) as batch_op:
# batch_op.drop_constraint(
- # "apc_validation_annee_etudid_formation_ordre_key", type_="unique"
+ # "apc_validation_annee_etudid_ordre_refcomp_key", type_="unique"
# )
- batch_op.create_unique_constraint(
- "apc_validation_annee_etudid_annee_scolaire_ordre_key",
- ["etudid", "annee_scolaire", "ordre"],
- )
-
- # ### end Alembic commands ###
+ # batch_op.drop_column("referentiel_competence_id")
+ batch_op.add_column(sa.Column("formation_id", sa.Integer(), nullable=True))
diff --git a/pytest.ini b/pytest.ini
index e4d9d0be..e92885fe 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,6 +1,7 @@
[pytest]
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
+ apo
but_gb
but_gccd
but_mlt
diff --git a/sco_version.py b/sco_version.py
index aa68bec3..832f5c16 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,13 +1,24 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
-SCOVERSION = "9.4.94"
+SCOVERSION = "9.4.96"
SCONAME = "ScoDoc"
SCONEWS = """
Année 2023
+
+- ScoDoc 9.6 (juillet 2023)
+
+ - Nouvelle gestion des absences et assiduité
+
+
+- ScoDoc 9.5 (juillet 2023)
+
+ - Version de maintenance (sécurité et correctifs critiques) sur Debian 11: fin de vie: 1/11/2023
+
+
- ScoDoc 9.4
- Connexion avec service CAS
diff --git a/tests/ressources/apogee/BUT-INFO-S2.txt b/tests/ressources/apogee/BUT-INFO-S2.txt
new file mode 100644
index 00000000..1970f0fe
--- /dev/null
+++ b/tests/ressources/apogee/BUT-INFO-S2.txt
@@ -0,0 +1,76 @@
+XX-APO_TITRES-XX
+apoC_annee 2021/2022
+apoC_cod_dip DIPTIS2
+apoC_Cod_Exp 2
+apoC_cod_vdi 17
+apoC_Fichier_Exp export.txt
+apoC_lib_dip BUT INFO TEST
+apoC_Titre1 Maquette pour tests unitaires sur un BUT Info S2
+apoC_Titre2
+
+XX-APO_TYP_RES-XX
+10 AB1 AB2 ABI ABJ ADM AJ AJRO C1 DEF DIF
+18 AB1 AB2 ABI ABJ ADM ADMC ADMD AJ AJAC AJAR AJRO ATT B1 C1 COMP DEF DIF NAR
+45 ABI ABJ ADAC ADM ADMC ADMD AIR AJ AJAR AJCP AJRO AJS ATT B1 B2 C1 COMP CRED DEF DES DETT DIF ENER ENRA EXC INFA INFO INST LC MACS N1 N2 NAR NON NSUI NVAL OUI SUIV SUPS TELE TOEF TOIE VAL VALC VALR
+10 ABI ABJ ADMC COMP DEF DIS NVAL VAL VALC VALR
+AB1 : Ajourné en B2 mais admis en B1 AB2 : ADMIS en B1 mais ajourné en B2 ABI : Absence ABJ : Absence justifiée ADM : Admis AJ : Ajourné AJRO : Ajourné - Réorientation Obligatoire C1 : Niveau C1 DEF : Défaillant DIF : Décision différée
+AB1 : Ajourné en B2 mais admis en B1 AB2 : ADMIS en B1 mais ajourné en B2 ABI : Absence ABJ : Absence justifiée ADM : Admis ADMC : Admis avec compensation ADMD : Admis (passage avec dette) AJ : Ajourné AJAC : Ajourné mais accès autorisé à étape sup. AJAR : Ajourné et Admis A Redoubler AJRO : Ajourné - Réorientation Obligatoire ATT : En attente de décison B1 : Niveau B1 C1 : Niveau C1 COMP : Compensé DEF : Défaillant DIF : Décision différée NAR : Ajourné non admis à redoubler
+ABI : Absence ABJ : Absence justifiée ADAC : Admis avant choix ADM : Admis ADMC : Admis avec compensation ADMD : Admis (passage avec dette) AIR : Ingénieur spécialité Informatique appr AJ : Ajourné AJAR : Ajourné et Admis A Redoubler AJCP : Ajourné mais autorisé à compenser AJRO : Ajourné - Réorientation Obligatoire AJS : Ajourné (note éliminatoire) ATT : En attente de décison B1 : Niveau B1 B2 : Niveau B2 C1 : Niveau C1 COMP : Compensé CRED : Eléments en crédits DEF : Défaillant DES : Désistement DETT : Eléments en dettes DIF : Décision différée ENER : Ingénieur spécialité Energétique ENRA : Ingénieur spécialité Energétique appr EXC : Exclu INFA : Ingénieur spécialité Informatique appr INFO : Ingénieur spécialié Informatique INST : Ingénieur spécialité Instrumentation LC : Liste complémentaire MACS : Ingénieur spécialité MACS N1 : Compétences CLES N2 : Niveau N2 NAR : Ajourné non admis à redoubler NON : Non NSUI : Non suivi(e) NVAL : Non Validé(e) OUI : Oui SUIV : Suivi(e) SUPS : Supérieur au seuil TELE : Ingénieur spéciailté Télécommunications TOEF : TOEFL TOIE : TOEIC VAL : Validé(e) VALC : Validé(e) par compensation VALR : Validé(e) Retrospectivement
+ABI : Absence ABJ : Absence justifiée ADMC : Admis avec compensation COMP : Compensé DEF : Défaillant DIS : Dispense examen NVAL : Non Validé(e) VAL : Validé(e) VALC : Validé(e) par compensation VALR : Validé(e) Retrospectivement
+
+XX-APO_COLONNES-XX
+apoL_a01_code Type Objet Code Version Année Session Admission/Admissibilité Type Rés. Etudiant Numéro
+apoL_a02_nom Nom
+apoL_a03_prenom Prénom
+apoL_a04_naissance Session Admissibilité Naissance
+APO_COL_VAL_DEB
+apoL_c0001 ELP V1INFU21 2021 0 1 N V1INFU21 - UE 2.1 Réaliser 0 1 Note
+apoL_c0002 ELP V1INFU21 2021 0 1 B 0 1 Barème
+apoL_c0003 ELP V1INFU21 2021 0 1 J 0 1 Pts Jury
+apoL_c0004 ELP V1INFU21 2021 0 1 R 0 1 Résultat
+apoL_c0005 ELP V1INFU22 2021 0 1 N V1INFU22 - UE 2.2 Optimiser 0 1 Note
+apoL_c0006 ELP V1INFU22 2021 0 1 B 0 1 Barème
+apoL_c0007 ELP V1INFU22 2021 0 1 J 0 1 Pts Jury
+apoL_c0008 ELP V1INFU22 2021 0 1 R 0 1 Résultat
+apoL_c0009 ELP V1INFU23 2021 0 1 N V1INFU23 - UE 2.3 Administrer 0 1 Note
+apoL_c0010 ELP V1INFU23 2021 0 1 B 0 1 Barème
+apoL_c0011 ELP V1INFU23 2021 0 1 J 0 1 Pts Jury
+apoL_c0012 ELP V1INFU23 2021 0 1 R 0 1 Résultat
+apoL_c0013 ELP V1INFU24 2021 0 1 N V1INFU24 - UE 2.4 Gérer 0 1 Note
+apoL_c0014 ELP V1INFU24 2021 0 1 B 0 1 Barème
+apoL_c0015 ELP V1INFU24 2021 0 1 J 0 1 Pts Jury
+apoL_c0016 ELP V1INFU24 2021 0 1 R 0 1 Résultat
+apoL_c0017 ELP V1INFU25 2021 0 1 N V1INFU25 - UE 2.5 Conduire 0 1 Note
+apoL_c0018 ELP V1INFU25 2021 0 1 B 0 1 Barème
+apoL_c0019 ELP V1INFU25 2021 0 1 J 0 1 Pts Jury
+apoL_c0020 ELP V1INFU25 2021 0 1 R 0 1 Résultat
+apoL_c0021 ELP V1INFU26 2021 0 1 N V1INFU26 - UE 2.6 Travailler 0 1 Note
+apoL_c0022 ELP V1INFU26 2021 0 1 B 0 1 Barème
+apoL_c0023 ELP V1INFU26 2021 0 1 J 0 1 Pts Jury
+apoL_c0024 ELP V1INFU26 2021 0 1 R 0 1 Résultat
+apoL_c0025 ELP VINFR201 2021 0 1 N VINFR201 - Développement orienté objets 0 1 Note
+apoL_c0026 ELP VINFR201 2021 0 1 B 0 1 Barème
+apoL_c0027 ELP VINFR207 2021 0 1 N VINFR207 - Graphes 0 1 Note
+apoL_c0028 ELP VINFR207 2021 0 1 B 0 1 Barème
+apoL_c0029 ELP VINFPOR2 2021 0 1 N VINFPOR2 - Portfolio 0 1 Note
+apoL_c0030 ELP VINFPOR2 2021 0 1 B 0 1 Barème
+apoL_c0031 ELP TIRW2 2021 0 1 N TIRW2 - Semestre 2 BUT INFO 2 0 1 Note
+apoL_c0032 ELP TIRW2 2021 0 1 B 0 1 Barème
+apoL_c0033 ELP TIRW2 2021 0 1 J 0 1 Pts Jury
+apoL_c0034 ELP TIRW2 2021 0 1 R 0 1 Résultat
+apoL_c0035 ELP TIRO 2021 0 1 N TIRO - Année BUT 1 RT 0 1 Note
+apoL_c0036 ELP TIRO 2021 0 1 B 0 1 Barème
+apoL_c0037 VET TI1 117 2021 0 1 N TI1 - BUT INFO an1 0 1 Note
+apoL_c0038 VET TI1 117 2021 0 1 B 0 1 Barème
+apoL_c0039 VET TI1 117 2021 0 1 J 0 1 Pts Jury
+apoL_c0040 VET TI1 117 2021 0 1 R 0 1 Résultat
+APO_COL_VAL_FIN
+apoL_c0041 APO_COL_VAL_FIN
+
+XX-APO_VALEURS-XX
+apoL_a01_code apoL_a02_nom apoL_a03_prenom apoL_a04_naissance apoL_c0001 apoL_c0002 apoL_c0003 apoL_c0004 apoL_c0005 apoL_c0006 apoL_c0007 apoL_c0008 apoL_c0009 apoL_c0010 apoL_c0011 apoL_c0012 apoL_c0013 apoL_c0014 apoL_c0015 apoL_c0016 apoL_c0017 apoL_c0018 apoL_c0019 apoL_c0020 apoL_c0021 apoL_c0022 apoL_c0023 apoL_c0024 apoL_c0025 apoL_c0026 apoL_c0027 apoL_c0028 apoL_c0029 apoL_c0030 apoL_c0031 apoL_c0032 apoL_c0033 apoL_c0034 apoL_c0035 apoL_c0036 apoL_c0037 apoL_c0038 apoL_c0039 apoL_c0040
+
+1001 ex_a1 Jean 10/01/2003
+1002 ex_a2 Lucie 11/01/2003
+1003 ex_b1 Hélène 11/01/2003
+1004 ex_b2 Rose 11/01/2003
diff --git a/tests/ressources/yaml/cursus_but_geii_lyon.yaml b/tests/ressources/yaml/cursus_but_geii_lyon.yaml
index 7ae5ecaa..fefdc9b6 100644
--- a/tests/ressources/yaml/cursus_but_geii_lyon.yaml
+++ b/tests/ressources/yaml/cursus_but_geii_lyon.yaml
@@ -109,6 +109,16 @@ FormSemestres:
idx: 1
date_debut: 2022-09-02
date_fin: 2023-01-12
+ S3:
+ idx: 3
+ codes_parcours: ['AII']
+ date_debut: 2022-09-01
+ date_fin: 2023-01-15
+ S4:
+ idx: 4
+ codes_parcours: ['AII']
+ date_debut: 2023-01-16
+ date_fin: 2023-07-10
Etudiants:
geii8:
@@ -1265,3 +1275,135 @@ Etudiants:
moy_ue: 13.5000
# decisions_rcues: aucun RCUE en S1-red
decision_annee: AJ
+ geii89:
+ prenom: etugeii89
+ civilite: M
+ formsemestres:
+ S1:
+ notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+ "S1.1": 13.5000
+ "S1.2": 13.0000
+ attendu: # les codes jury que l'on doit vérifier
+ deca:
+ passage_de_droit: False
+ nb_competences: 2
+ nb_rcue_annee: 0
+ decisions_ues:
+ "UE11":
+ codes: [ "ADM", "..." ]
+ code_valide: ADM
+ decision_jury: ATJ # A cause des absences
+ moy_ue: 13.5000
+ "UE12":
+ codes: [ "ADM", "..." ]
+ code_valide: ADM
+ decision_jury: ATJ # A cause des absences
+ moy_ue: 13.0000
+ S2:
+ notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+ "S2.1": 14.5000
+ "S2.2": 14.0000
+ attendu: # les codes jury que l'on doit vérifier
+ deca:
+ passage_de_droit: True # d'apres les notes, on *pourrait* passer
+ autorisations_inscription: [2] # et le jury manuel nous fait passer
+ nb_competences: 2
+ nb_rcue_annee: 2
+ valide_moitie_rcue: False
+ codes: [ "ATJ", "..." ]
+ decisions_ues:
+ "UE21":
+ codes: [ "ADM", "..." ]
+ code_valide: ADM
+ decision_jury: ATJ
+ moy_ue: 14.5000
+ "UE22":
+ codes: [ "ADM", "..." ]
+ code_valide: ADM
+ decision_jury: ATJ
+ moy_ue: 14.0000
+ decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
+ "UE11":
+ code_valide: ADM # le code proposé en auto
+ decision_jury: ATJ # le code forcé manuellement par le jury
+ rcue:
+ # moy_rcue: 14.0000 # Pas de moyenne calculée
+ est_compensable: False
+ "UE12":
+ code_valide: ADM # le code proposé en auto
+ decision_jury: ATJ # le code forcé manuellement par le jury
+ rcue:
+ # moy_rcue: 13.5000 # Pas de moyenne calculée
+ est_compensable: False
+ decision_annee: ATJ # Passage tout de même en S3
+ #
+ # ----------------------- geii90 : ADSUP envoyés par BUT2 vers BUT1
+ #
+ geii90:
+ prenom: etugeii90
+ civilite: M
+ code_nip: geii90
+ formsemestres:
+ S1: # 2 UEs, les deux en AJ
+ notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+ "S1.1": 9.5000
+ "S1.2": 8.5000
+ attendu: # les codes jury que l'on doit vérifier
+ deca:
+ passage_de_droit: False
+ nb_competences: 2
+ nb_rcue_annee: 0
+ decisions_ues:
+ "UE11":
+ codes: [ "AJ", "..." ]
+ "UE12":
+ codes: [ "AJ", "..." ]
+ S2: # pareil, mais le jury le fait passer en S3
+ notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
+ "S2.1": 9.8000
+ "S2.2": 9.9000
+ attendu: # les codes jury que l'on doit vérifier
+ deca:
+ passage_de_droit: False # d'apres les notes, on ne peut pas passer
+ autorisations_inscription: [2] # et le jury manuel nous fait passer
+ nb_competences: 2
+ nb_rcue_annee: 2
+ valide_moitie_rcue: False
+ codes: [ "ADJ", "ATJ", "RED", "..." ]
+ code_valide: RED # le code proposé en auto
+ decisions_ues:
+ "UE21":
+ codes: [ "AJ", "..." ]
+ code_valide: AJ
+ moy_ue: 9.8
+ "UE22":
+ code_valide: AJ
+ moy_ue: 9.9
+ decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
+ "UE11":
+ code_valide: AJ # le code proposé en auto
+ rcue:
+ # moy_rcue: 14.0000 # Pas de moyenne calculée
+ est_compensable: False
+ "UE12":
+ code_valide: AJ # le code proposé en auto
+ rcue:
+ # moy_rcue: 13.5000 # Pas de moyenne calculée
+ est_compensable: False
+ decision_annee: ADJ # Passage tout de même en S3 !
+ S3: # le S3 avec 4 niveaux
+ parcours: AII
+ notes_modules: # combinaison pour avoir ADM AJ AJ AJ
+ "AII3": 9
+ "ER3": 10.75
+ "AU3": 8
+ attendu: # les codes jury que l'on doit vérifier
+ deca:
+ passage_de_droit: False # d'apres les notes, on ne peut pas passer
+ autorisations_inscription: [4] # passe en S4
+ nb_competences: 4
+ S4: # le S4 avec 4 niveaux
+ parcours: AII
+ notes_modules: # combinaison pour avoir ADM ADM ADM AJ
+ "PF4": 12
+ "SAE4AII": 8
diff --git a/tests/ressources/yaml/cursus_but_info.yaml b/tests/ressources/yaml/cursus_but_info.yaml
index f735e1e5..52ab3759 100644
--- a/tests/ressources/yaml/cursus_but_info.yaml
+++ b/tests/ressources/yaml/cursus_but_info.yaml
@@ -1,4 +1,4 @@
-# Tests unitaires
+# Tests unitaires
# Le BUT Info a 4 parcours qui partagent certains niveaux de compétences
# mais à ces niveaux sont associés des UEs dont les coefficients des ressources
# varient selon le parcours.
@@ -14,58 +14,58 @@ Formation:
# nota: les associations UE/Niveaux sont déjà données dans ce fichier XML.
ues:
# S1
- 'UE11':
+ "UE11":
annee: BUT1
- 'UE12':
+ "UE12":
annee: BUT1
- 'UE13':
+ "UE13":
annee: BUT1
- 'UE14':
+ "UE14":
annee: BUT1
- 'UE15':
+ "UE15":
annee: BUT1
- 'UE16':
+ "UE16":
annee: BUT1
# S2
- 'UE21':
+ "UE21":
annee: BUT1
- 'UE22':
+ "UE22":
annee: BUT1
- 'UE23':
+ "UE23":
annee: BUT1
- 'UE24':
+ "UE24":
annee: BUT1
- 'UE25':
+ "UE25":
annee: BUT1
- 'UE26':
+ "UE26":
annee: BUT1
# S3
- 'UE31':
+ "UE31":
annee: BUT2
- 'UE32':
+ "UE32":
annee: BUT2
- 'UE33':
+ "UE33":
annee: BUT2
- 'UE34':
+ "UE34":
annee: BUT2
- 'UE35':
+ "UE35":
annee: BUT2
- 'UE36':
+ "UE36":
annee: BUT2
# S4
- 'UE41-A': # UE pour le parcours A
+ "UE41-A": # UE pour le parcours A
annee: BUT2
- 'UE41-B': # UE pour le parcours B (même contenu, coefs différents)
+ "UE41-B": # UE pour le parcours B (même contenu, coefs différents)
annee: BUT2
- 'UE42':
+ "UE42":
annee: BUT2
- 'UE43':
+ "UE43":
annee: BUT2
- 'UE44':
+ "UE44":
annee: BUT2
- 'UE45':
+ "UE45":
annee: BUT2
- 'UE46':
+ "UE46":
annee: BUT2
FormSemestres:
@@ -74,37 +74,41 @@ FormSemestres:
idx: 1
date_debut: 2021-09-01
date_fin: 2022-01-15
- codes_parcours: ['A', 'B']
+ codes_parcours: ["A", "B"]
S2:
idx: 2
date_debut: 2022-01-16
date_fin: 2022-06-30
- codes_parcours: ['A', 'B']
+ codes_parcours: ["A", "B"]
+ elt_sem_apo: TIRW2
+ elt_annee_apo: TIRO
+ etape_apo: TI1!117
S3:
idx: 3
date_debut: 2022-09-01
date_fin: 2023-01-15
- codes_parcours: ['A', 'B']
+ codes_parcours: ["A", "B"]
S4:
idx: 4
date_debut: 2023-01-16
date_fin: 2023-06-30
- codes_parcours: ['A', 'B']
+ codes_parcours: ["A", "B"]
S5:
idx: 5
date_debut: 2023-09-01
date_fin: 2024-01-15
- codes_parcours: ['A', 'B']
+ codes_parcours: ["A", "B"]
S6:
idx: 6
date_debut: 2024-01-16
date_fin: 2024-06-30
- codes_parcours: ['A', 'B']
+ codes_parcours: ["A", "B"]
Etudiants:
ex_a1: # cursus S1 -> S6, valide tout
prenom: Jean
civilite: M
+ code_nip: 1001
formsemestres:
# on ne note que le portfolio, qui affecte toutes les UEs
S1:
@@ -115,6 +119,7 @@ Etudiants:
parcours: A
notes_modules:
"P2": 12
+ "R2.04-A": 16
S3:
parcours: A
notes_modules:
@@ -135,6 +140,7 @@ Etudiants:
ex_a2: # cursus S1 -> S6, valide tout sauf S5
prenom: Lucie
civilite: F
+ code_nip: 1002
formsemestres:
# on ne note que le portfolio, qui affecte toutes les UEs
S1:
@@ -145,6 +151,7 @@ Etudiants:
parcours: A
notes_modules:
"P2": 12
+ "R2.04-A": 17
S3:
parcours: A
notes_modules:
@@ -161,10 +168,11 @@ Etudiants:
parcours: A
notes_modules:
"P6-A": 16
-
+
ex_b1: # cursus S1 -> S6, valide tout
prenom: Hélène
civilite: F
+ code_nip: 1003
formsemestres:
# on ne note que le portfolio, qui affecte toutes les UEs
S1:
@@ -175,6 +183,7 @@ Etudiants:
parcours: B
notes_modules:
"P2": 12
+ "R2.04-B": 18
S3:
parcours: B
notes_modules:
@@ -191,10 +200,11 @@ Etudiants:
parcours: B
notes_modules:
"P6-B": 16
-
+
ex_b2: # cursus S1 -> S6, valide tout sauf S6
prenom: Rose
civilite: F
+ code_nip: 1004
formsemestres:
# on ne note que le portfolio, qui affecte toutes les UEs
S1:
@@ -205,6 +215,7 @@ Etudiants:
parcours: B
notes_modules:
"P2": 12
+ "R2.04-B": 19
S3:
parcours: B
notes_modules:
diff --git a/tests/unit/test_apogee_export.py b/tests/unit/test_apogee_export.py
new file mode 100644
index 00000000..4b84f0a8
--- /dev/null
+++ b/tests/unit/test_apogee_export.py
@@ -0,0 +1,54 @@
+##############################################################################
+# ScoDoc
+# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
+# See LICENSE
+##############################################################################
+
+""" Test export Apogéee
+
+Ces tests sont généralement lents (construction de la base),
+et donc marqués par `@pytest.mark.slow`.
+
+Certains sont aussi marqués par @pytest.mark.lemans ou @pytest.mark.lyon
+pour lancer certains tests spécifiques seulement.
+
+Exemple utilisation spécifique:
+# test sur "apo" seulement:
+pytest --pdb -m apo tests/unit/test_apogee_export.py
+
+Elements Apogée simulés:
+
+- UEs : TIU2x
+- Ressources: R2.xy : TIRxy (VRETR201 -> TIR201)
+"""
+
+import pytest
+from tests.unit import yaml_setup, yaml_setup_but
+
+import app
+from app.but.jury_but_validation_auto import formsemestre_validation_auto_but
+from app.models import Formation, FormSemestre, UniteEns
+from config import TestConfig
+
+DEPT = TestConfig.DEPT_TEST
+
+
+@pytest.mark.skip # Ce "test" est utilisé comme setup pour développer, pas comme test unitaire routinier
+@pytest.mark.slow
+@pytest.mark.apo
+def test_refcomp_niveaux_info(test_client):
+ """Test niveaux / parcours / UE pour un BUT INFO
+ avec parcours A et B, même compétences mais coefs différents
+ selon le parcours.
+ """
+ # WIP
+ # pour le moment juste le chargement de la formation, du ref. comp, et des UE du S4.
+ app.set_sco_dept(DEPT)
+ doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
+ "tests/ressources/yaml/cursus_but_info.yaml"
+ )
+ for formsemestre_titre in formsemestre_titres:
+ formsemestre = yaml_setup.create_formsemestre_with_etuds(
+ doc, formation, formsemestre_titre
+ )
+ #
diff --git a/tests/unit/test_but_cursus.py b/tests/unit/test_but_cursus.py
index 12b54be1..293e6d24 100644
--- a/tests/unit/test_but_cursus.py
+++ b/tests/unit/test_but_cursus.py
@@ -31,7 +31,13 @@ def test_cursus_but_jury_gb(test_client):
app.set_sco_dept(DEPT)
# login_user(User.query.filter_by(user_name="admin").first()) # XXX pour tests manuels
# ctx.push() # XXX
- doc = yaml_setup.setup_from_yaml("tests/ressources/yaml/cursus_but_gb.yaml")
+ doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
+ "tests/ressources/yaml/cursus_but_gb.yaml"
+ )
+ for formsemestre_titre in formsemestre_titres:
+ formsemestre = yaml_setup.create_formsemestre_with_etuds(
+ doc, formation, formsemestre_titre
+ )
formsemestre: FormSemestre = FormSemestre.query.filter_by(titre="S3").first()
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
cursus = FormSemestreCursusBUT(res)
@@ -72,7 +78,13 @@ def test_refcomp_niveaux_info(test_client):
# WIP
# pour le moment juste le chargement de la formation, du ref. comp, et des UE du S4.
app.set_sco_dept(DEPT)
- doc = yaml_setup.setup_from_yaml("tests/ressources/yaml/cursus_but_info.yaml")
+ doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
+ "tests/ressources/yaml/cursus_but_info.yaml"
+ )
+ for formsemestre_titre in formsemestre_titres:
+ formsemestre = yaml_setup.create_formsemestre_with_etuds(
+ doc, formation, formsemestre_titre
+ )
formsemestre: FormSemestre = FormSemestre.query.filter_by(titre="S4").first()
assert formsemestre
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
diff --git a/tests/unit/test_formsemestre.py b/tests/unit/test_formsemestre.py
index 02945044..1af482eb 100644
--- a/tests/unit/test_formsemestre.py
+++ b/tests/unit/test_formsemestre.py
@@ -44,9 +44,18 @@ def test_formsemestres_associate_new_version(test_client):
app.set_sco_dept(DEPT)
# Construit la base de test GB une seule fois
# puis lance les tests de jury
- yaml_setup.setup_from_yaml("tests/ressources/yaml/simple_formsemestres.yaml")
- formation = Formation.query.filter_by(acronyme="BUT GEII", version=1).first()
+ doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
+ "tests/ressources/yaml/simple_formsemestres.yaml"
+ )
+ for formsemestre_titre in formsemestre_titres:
+ formsemestre = yaml_setup.create_formsemestre_with_etuds(
+ doc, formation, formsemestre_titre
+ )
+ assert formsemestre
+ formation_geii = Formation.query.filter_by(acronyme="BUT GEII", version=1).first()
+ assert formation_geii.id == formation.id
formsemestres = formation.formsemestres.all()
+ assert len(formsemestres) == len(formsemestre_titres)
# On a deux S1:
assert len(formsemestres) == 2
assert {s.semestre_id for s in formsemestres} == {1}
@@ -70,7 +79,14 @@ def test_formsemestre_misc_views(test_client):
Note: les anciennes vues renvoient souvent des str au lieu de Response.
"""
app.set_sco_dept(DEPT)
- yaml_setup.setup_from_yaml("tests/ressources/yaml/simple_formsemestres.yaml")
+ doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
+ "tests/ressources/yaml/simple_formsemestres.yaml"
+ )
+ for formsemestre_titre in formsemestre_titres:
+ formsemestre = yaml_setup.create_formsemestre_with_etuds(
+ doc, formation, formsemestre_titre
+ )
+ assert formsemestre
formsemestre: FormSemestre = FormSemestre.query.first()
# ----- MENU SEMESTRE
diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py
index bf4dc70e..277728e4 100644
--- a/tests/unit/yaml_setup.py
+++ b/tests/unit/yaml_setup.py
@@ -99,6 +99,9 @@ def create_formsemestre(
titre: str,
date_debut: str,
date_fin: str,
+ elt_sem_apo: str = None,
+ elt_annee_apo: str = None,
+ etape_apo: str = None,
) -> FormSemestre:
"Création d'un formsemestre, avec ses modimpls et évaluations"
assert formation.is_apc() or not parcours # parcours seulement si APC
@@ -110,11 +113,15 @@ def create_formsemestre(
semestre_id=semestre_id,
date_debut=date_debut,
date_fin=date_fin,
+ elt_sem_apo=elt_sem_apo,
+ elt_annee_apo=elt_annee_apo,
)
# set responsable (list)
a_user = User.query.first()
formsemestre.responsables = [a_user]
db.session.add(formsemestre)
+ db.session.flush()
+ formsemestre.add_etape(etape_apo)
# Ajoute tous les modules du semestre sans parcours OU avec l'un des parcours indiqués
sem_parcours_ids = {p.id for p in parcours}
modules = [
@@ -228,6 +235,10 @@ def setup_formsemestre(
assert parcour is not None
parcours.append(parcour)
+ elt_sem_apo = infos.get("elt_sem_apo")
+ elt_annee_apo = infos.get("elt_annee_apo")
+ etape_apo = infos.get("etape_apo")
+
formsemestre = create_formsemestre(
formation,
parcours,
@@ -235,6 +246,9 @@ def setup_formsemestre(
formsemestre_titre,
infos["date_debut"],
infos["date_fin"],
+ elt_sem_apo=elt_sem_apo,
+ elt_annee_apo=elt_annee_apo,
+ etape_apo=etape_apo,
)
db.session.flush()
@@ -257,6 +271,7 @@ def inscrit_les_etudiants(doc: dict, formsemestre_titre: str = ""):
# Création des étudiants (sauf si déjà existants)
prenom = infos.get("prenom", "prénom")
civilite = infos.get("civilite", "X")
+ code_nip = infos.get("code_nip", None)
etud = Identite.query.filter_by(
nom=nom, prenom=prenom, civilite=civilite
).first()
@@ -266,6 +281,7 @@ def inscrit_les_etudiants(doc: dict, formsemestre_titre: str = ""):
nom=nom,
prenom=prenom,
civilite=civilite,
+ code_nip=code_nip,
)
db.session.add(etud)
db.session.commit()