forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
@ -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)
|
||||
|
@ -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"),
|
||||
|
@ -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: <b>{
|
||||
', '.join(ue.acronyme for ue in ues_but1_non_validees)
|
||||
}</b>. """
|
||||
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.<br> "
|
||||
)
|
||||
else:
|
||||
explanation += (
|
||||
f"Compétence {niveau_but1} de BUT 1 validée par ce BUT2.<br> "
|
||||
)
|
||||
else:
|
||||
ok = False
|
||||
explanation += f"""Compétence {
|
||||
niveau_but1} de BUT 1 non validée et non existante en BUT2.<br> """
|
||||
|
||||
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
|
||||
|
@ -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}<br><br>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
|
||||
|
@ -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
|
||||
|
||||
<p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,2 point
|
||||
sur toutes les moyennes d'UE.
|
||||
<p>Le bonus est compris entre 0 et 0,2 points.
|
||||
et est reporté sur les moyennes d'UE.
|
||||
</p>
|
||||
<p>La valeur saisie doit être entre 0 et 0,2: toute valeur
|
||||
supérieure à 0,2 entraine un bonus de 0,2.
|
||||
</p>
|
||||
"""
|
||||
|
||||
@ -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)
|
||||
|
||||
<p>Nous avons deux types de bonifications : sport et/ou culture
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
||||
@ -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 {
|
||||
|
@ -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 <b>année BUT{self.ordre}</b> émise par
|
||||
{self.formsemestre.html_link_status() if self.formsemestre else "-"}
|
||||
{link}
|
||||
: <b>{self.code}</b>
|
||||
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:
|
||||
|
@ -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"""<a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.id,)
|
||||
}">{self.titre_mois()}</a>
|
||||
}" title="{title or ''}">{label or self.titre_mois()}</a>
|
||||
"""
|
||||
|
||||
@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):
|
||||
|
@ -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
|
||||
{'<span class="redboldtext">externe</span>' if self.is_external else ""}
|
||||
de l'UE <b>{self.ue.acronyme}</b>
|
||||
@ -101,9 +110,7 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
+ ", ".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 "externe/antérieure"}
|
||||
{("émise par " + link)}
|
||||
: <b>{self.code}</b>{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 <b>S{self.semestre_id}</b> é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")}
|
||||
"""
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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"<gen_table( nrows={self.get_nb_rows()}, ncols={self.get_nb_cols()} )>"
|
||||
|
||||
def __len__(self):
|
||||
return len(self.rows)
|
||||
|
||||
def get_nb_cols(self):
|
||||
return len(self.columns_ids)
|
||||
|
||||
|
@ -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 ? <br>colonne <tt>{col_id}</tt> 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()
|
||||
|
@ -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 ? (<tt>{i}/{field}</tt>))",
|
||||
filename=self.get_filename(),
|
||||
) from exc
|
||||
etud_tuples.append(
|
||||
ApoEtudTuple(
|
||||
nip=fields[0], # id etudiant
|
||||
|
@ -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()}
|
||||
</h2>
|
||||
|
||||
<p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement,
|
||||
<p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
|
||||
<em>dans un semestre hors ScoDoc</em>.</p>
|
||||
<p class="expl"><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
|
||||
(<em>pour les semestres précédents gérés avec ScoDoc,
|
||||
passer par la page jury normale)</em>).
|
||||
<p class="expl"><b>Les UE validées dans ScoDoc sont
|
||||
automatiquement prises en compte</b>.
|
||||
</p>
|
||||
<p>Cette page est surtout utile pour les étudiants ayant
|
||||
suivi un début de cursus dans <b>un autre établissement</b>, ou qui
|
||||
ont suivi une UE à l'étranger ou dans un semestre géré <b>sans ScoDoc</b>.
|
||||
</p>
|
||||
<p>Pour les semestres précédents gérés avec ScoDoc, passer par la page jury normale.
|
||||
</p>
|
||||
<p>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).
|
||||
</p>
|
||||
<p>Notez que l'UE est validée (ADM), avec enregistrement immédiat de la décision et
|
||||
l'attribution des ECTS.</p>
|
||||
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
|
||||
|
||||
{_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:
|
||||
<div class="help">Liste de toutes les UEs validées par {etud.html_link_fiche()},
|
||||
sur des semestres ou déclarées comme "antérieures" (externes).
|
||||
</div>
|
||||
<ul>"""
|
||||
<ul class="liste_validations">"""
|
||||
]
|
||||
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" (<b>S{validation.semestre_id}</b>)"
|
||||
H.append(
|
||||
f"""
|
||||
<li>{validation.html()}
|
||||
H.append(f"""<li>{validation.html()}""")
|
||||
if validation.formsemestre.can_edit_jury():
|
||||
H.append(
|
||||
f"""
|
||||
<form class="inline-form">
|
||||
<button
|
||||
data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}"
|
||||
>effacer</button>
|
||||
</form>
|
||||
</li>
|
||||
""",
|
||||
)
|
||||
""",
|
||||
)
|
||||
else:
|
||||
H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé"))
|
||||
H.append("</li>")
|
||||
H.append("</ul></div>")
|
||||
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
|
||||
)
|
||||
|
@ -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"]] = (
|
||||
"<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
@ -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) {
|
||||
|
@ -9,38 +9,41 @@
|
||||
{% block app_content %}
|
||||
|
||||
<div class="sco_help">
|
||||
<h2>Calcul automatique des décisions de jury du BUT</h2>
|
||||
<ul>
|
||||
<li>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.
|
||||
</li>
|
||||
|
||||
<li><b>Attention: peut modifier des décisions déjà enregistrées</b>, si la
|
||||
validation de droit est calculée. Par exemple, vous aviez saisi <b>RAT</b>
|
||||
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 <b>RAT</b> par un <b>ADM</b>, ScoDoc considérant que les
|
||||
conditions sont satisfaites. On peut éviter cela en laissant une note de
|
||||
l'étudiant en ATTente.
|
||||
</li>
|
||||
|
||||
<li>N'enregistre que les décisions <b>validantes de droit: ADM ou CMP</b>.
|
||||
</li>
|
||||
<li>N'enregistre pas de décision si l'étudiant a une ou plusieurs notes en ATTente.
|
||||
</li>
|
||||
<li>L'assiduité n'est <b>pas</b> prise en compte. </li>
|
||||
</ul>
|
||||
<p>
|
||||
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
|
||||
notamment sur les UEs en dessous de 10.
|
||||
</p>
|
||||
<div class="warning">
|
||||
<h2>Calcul automatique des décisions de jury du BUT</h2>
|
||||
<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>
|
||||
<li>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.
|
||||
</li>
|
||||
|
||||
<li><b>Attention: peut modifier des décisions déjà enregistrées</b>, si la
|
||||
validation de droit est calculée.
|
||||
Ce calcul <b>n'utilise que les notes, et pas les décisions manuelles déjà saisies.</b>
|
||||
<br>
|
||||
Par exemple, vous aviez saisi <b>ATJ</b> ou <b>RAT</b>
|
||||
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 <b>RAT</b> par un <b>ADM</b>, ScoDoc considérant que les
|
||||
conditions sont satisfaites. On peut éviter cela en laissant une note de
|
||||
l'étudiant en ATTente.
|
||||
</li>
|
||||
|
||||
<li>N'enregistre que les décisions <b>validantes de droit: ADM ou CMP</b>.
|
||||
</li>
|
||||
<li>N'enregistre pas de décision si l'étudiant a une ou plusieurs notes en ATTente.
|
||||
</li>
|
||||
<li>L'assiduité n'est <b>pas</b> prise en compte. </li>
|
||||
</ul>
|
||||
<p>
|
||||
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
|
||||
notamment sur les UEs en dessous de 10.
|
||||
</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>
|
||||
|
||||
|
@ -4,8 +4,8 @@
|
||||
|
||||
{% 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>
|
||||
sur <b>l'année {{annee}}</b>
|
||||
de la formation <em>{{ formation.html() }}</em>
|
||||
</p>
|
||||
|
||||
<div style="margin-top: 16px;">
|
||||
@ -16,7 +16,7 @@ de la formation <em>{{ formation.html() }}</em>
|
||||
<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>
|
||||
quelle que soit leur origine.</p>
|
||||
|
||||
<p>Les décisions concernées sont:</p>
|
||||
<ul>
|
||||
@ -34,8 +34,34 @@ quelle que soit leur origine.</p>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<div class="sco_box">
|
||||
<div class="sco_box_title">Autres actions:</div>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('notes.jury_delete_manual',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id
|
||||
)
|
||||
}}">effacer les décisions une à une</a>
|
||||
</li>
|
||||
{% if formsemestre_origine is not none %}
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('notes.formsemestre_jury_but_erase',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_origine.id,
|
||||
etudid=etud.id, only_one_sem=1)
|
||||
}}">
|
||||
effacer seulement les décisions émises par le semestre
|
||||
{{formsemestre_origine.titre_formation(with_sem_idx=1)|safe}}
|
||||
(efface aussi la décision annuelle)
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
@ -2534,21 +2534,20 @@ def formsemestre_validation_but(
|
||||
</div>"""
|
||||
)
|
||||
else:
|
||||
erase_span = f"""<a href="{
|
||||
url_for("notes.formsemestre_jury_but_erase",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre.id,
|
||||
etudid=deca.etud.id)}" class="stdlink"
|
||||
title="efface décisions issues des jurys de cette année"
|
||||
>effacer décisions de ce jury</a>
|
||||
|
||||
erase_span = f"""
|
||||
<a style="margin-left: 16px;" class="stdlink"
|
||||
href="{
|
||||
url_for("notes.erase_decisions_annee_formation",
|
||||
scodoc_dept=g.scodoc_dept, formation_id=deca.formsemestre.formation.id,
|
||||
etudid=deca.etud.id, annee=deca.annee_but)}"
|
||||
title="efface toutes décisions concernant le BUT{deca.annee_but}
|
||||
de cet étudiant (même extérieures ou issues d'un redoublement)"
|
||||
>effacer toutes ses décisions de BUT{deca.annee_but}</a>
|
||||
etudid=deca.etud.id, annee=deca.annee_but, formsemestre_id=formsemestre_id)}"
|
||||
>effacer des décisions de jury</a>
|
||||
|
||||
<a style="margin-left: 16px;" class="stdlink"
|
||||
href="{
|
||||
url_for("notes.formsemestre_validate_previous_ue",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=deca.etud.id, formsemestre_id=formsemestre_id)}"
|
||||
>enregistrer des UEs antérieures</a>
|
||||
"""
|
||||
H.append(
|
||||
f"""<div class="but_settings">
|
||||
@ -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}",
|
||||
|
@ -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))
|
||||
|
@ -1,6 +1,7 @@
|
||||
[pytest]
|
||||
markers =
|
||||
slow: marks tests as slow (deselect with '-m "not slow"')
|
||||
apo
|
||||
but_gb
|
||||
but_gccd
|
||||
but_mlt
|
||||
|
@ -1,13 +1,24 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.4.94"
|
||||
SCOVERSION = "9.4.96"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
SCONEWS = """
|
||||
<h4>Année 2023</h4>
|
||||
<ul>
|
||||
|
||||
<li>ScoDoc 9.6 (juillet 2023)</li>
|
||||
<ul>
|
||||
<li>Nouvelle gestion des absences et assiduité</li>
|
||||
</ul>
|
||||
|
||||
<li>ScoDoc 9.5 (juillet 2023)</li>
|
||||
<ul>
|
||||
<li>Version de maintenance (sécurité et correctifs critiques) sur Debian 11: fin de vie: 1/11/2023</li>
|
||||
</ul>
|
||||
|
||||
<li>ScoDoc 9.4</li>
|
||||
<ul>
|
||||
<li>Connexion avec service CAS</li>
|
||||
|
76
tests/ressources/apogee/BUT-INFO-S2.txt
Normal file
76
tests/ressources/apogee/BUT-INFO-S2.txt
Normal file
@ -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
|
@ -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
|
||||
|
@ -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:
|
||||
|
54
tests/unit/test_apogee_export.py
Normal file
54
tests/unit/test_apogee_export.py
Normal file
@ -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
|
||||
)
|
||||
#
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user