diff --git a/app/api/jury.py b/app/api/jury.py index 6103d1164..6f0710770 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 5eda000dc..97a555ca6 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 97ab95aca..1ec6eeeaf 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 29630af04..d0933c7a6 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 c9c9348bd..d3e8db03e 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 1c43158da..ee2c13738 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 163d56f1d..b2ba3eb5c 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 824e7e3b6..b33e2f2bb 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 997d1a46f..185ab5398 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 37e09d140..d43bb09bd 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 9e2cf5e27..d4ca5bb07 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 85d14b957..dff4ead7b 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 436ecde25..2b5e867a9 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 1348cbe8c..61af26bdd 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 1e7984336..2a56d85d9 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 f145b3f2c..231094475 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).
- +

+ 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 0c231b532..4549688b5 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 d6a77c614..4bc79ebae 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 8955a1a3d..79152d570 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 e4d9d0bed..e92885fe6 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 aa68bec3d..832f5c16d 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 000000000..1970f0fef --- /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 7ae5ecaae..fefdc9b6b 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 f735e1e56..52ab37599 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 000000000..4b84f0a86 --- /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 12b54be1b..293e6d242 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 029450444..1af482eb1 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 bf4dc70e4..277728e47 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()