diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index 7b20ffd51..da7dffaba 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -322,7 +322,8 @@ class NotesTableCompat(ResultatsSemestre): def get_etud_decision_sem(self, etudid: int) -> dict: """Decision du jury semestre prise pour cet etudiant, ou None s'il n'y en pas eu. { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id } - Si état défaillant, force le code a DEF + Si état défaillant, force le code a DEF. + Toujours None en BUT. """ if self.get_etud_etat(etudid) == DEF: return { diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 648605220..42e8b1c8a 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -265,7 +265,6 @@ class ApoEtud(dict): "Vrai si BUT" self.col_elts = {} "{'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}" - self.new_cols = {} # { col_id : value to record in csv } self.etud: Identite = None "etudiant ScoDoc associé" self.etat = None # ETUD_OK, ... @@ -283,6 +282,17 @@ class ApoEtud(dict): self.fmt_note = functools.partial( _apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f" ) + # Initialisés par associate_sco: + self.autre_sem: dict = None + self.autre_res: NotesTableCompat = None + self.cur_sem: dict = None + self.cur_res: NotesTableCompat = None + self.new_cols = {} + "{ col_id : value to record in csv }" + + # Pour le BUT: + self.validation_annee_but: ApcValidationAnnee = None + "validation de jury annuelle BUT, ou None" def __repr__(self): return f"""ApoEtud( nom='{self["nom"]}', nip='{self["nip"]}' )""" @@ -336,18 +346,17 @@ class ApoEtud(dict): sco_elts = {} # valeurs trouvées dans ScoDoc code : { N, B, J, R } for col_id in apo_data.col_ids[4:]: code = apo_data.cols[col_id]["Code"] # 'V1RT' - el = sco_elts.get( - code, None - ) # {'R': ADM, 'J': '', 'B': 20, 'N': '12.14'} - if el is None: # pas déjà trouvé - cur_sem, autre_sem = self.etud_semestres_de_etape(apo_data) + elt = sco_elts.get(code, None) + # elt est {'R': ADM, 'J': '', 'B': 20, 'N': '12.14'} + if elt is None: # pas déjà trouvé + self.etud_set_semestres_de_etape(apo_data) for sem in apo_data.sems_etape: - el = self.search_elt_in_sem(code, sem, cur_sem, autre_sem) - if el is not None: - sco_elts[code] = el + elt = self.search_elt_in_sem(code, sem) + if elt is not None: + sco_elts[code] = elt break - self.col_elts[code] = el - if el is None: + self.col_elts[code] = elt + if elt is None: self.new_cols[col_id] = self.cols[col_id] else: try: @@ -373,7 +382,7 @@ class ApoEtud(dict): # codes = set([apo_data.cols[col_id].code for col_id in apo_data.col_ids]) # return codes - set(sco_elts) - def search_elt_in_sem(self, code, sem, cur_sem, autre_sem) -> dict: + def search_elt_in_sem(self, code, sem) -> dict: """ VET code jury etape (en BUT, le code annuel) ELP élément pédagogique: UE, module @@ -387,20 +396,29 @@ class ApoEtud(dict): code (str): code apo de l'element cherché sem (dict): semestre dans lequel on cherche l'élément cur_sem (dict): semestre "courant" pour résultats annuels (VET) - autre_sem (dict): autre semestre utilisé pour calculé les résultats annuels (VET) + autre_sem (dict): autre semestre utilisé pour calculer les résultats annuels (VET) Returns: dict: with N, B, J, R keys, ou None si elt non trouvé """ etudid = self.etud["etudid"] - formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + if not self.cur_res: + log("search_elt_in_sem: no cur_res !") + return None + if sem["formsemestre_id"] == self.cur_res.formsemestre.id: + res = self.cur_res + elif ( + self.autre_res and sem["formsemestre_id"] == self.autre_res.formsemestre.id + ): + res = self.autre_res + else: + formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) + res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - if etudid not in nt.identdict: + if etudid not in res.identdict: return None # etudiant non inscrit dans ce semestre - decision = nt.get_etud_decision_sem(etudid) - if not self.export_res_sdj and not decision: + if not self.export_res_sdj and not res.etud_has_decision(etudid): # pas de decision de jury, on n'enregistre rien # (meme si démissionnaire) if not self.has_logged_no_decision: @@ -408,43 +426,46 @@ class ApoEtud(dict): self.has_logged_no_decision = True return VOID_APO_RES - if decision and decision["code"] == NAR: - self.is_NAR = True + if res.is_apc: # export BUT + self._but_load_validation_annuelle() + else: + decision = res.get_etud_decision_sem(etudid) + if decision and decision["code"] == NAR: + self.is_NAR = True + # Element semestre: (non BUT donc) + if code in {x.strip() for x in sem["elt_sem_apo"].split(",")}: + if self.export_res_sem: + return self.comp_elt_semestre(res, decision, etudid) + else: + return VOID_APO_RES # Element etape (annuel ou non): if sco_formsemestre.sem_has_etape(sem, code) or ( code in {x.strip() for x in sem["elt_annee_apo"].split(",")} ): export_res_etape = self.export_res_etape - if (not export_res_etape) and cur_sem: + if (not export_res_etape) and self.cur_sem: # exporte toujours le résultat de l'étape si l'étudiant est diplômé Se = sco_cursus.get_situation_etud_cursus( - self.etud, cur_sem["formsemestre_id"] + self.etud, self.cur_sem["formsemestre_id"] ) export_res_etape = Se.all_other_validated() if export_res_etape: - return self.comp_elt_annuel(etudid, cur_sem, autre_sem) + return self.comp_elt_annuel(etudid) else: self.log.append("export étape désactivé") return VOID_APO_RES - # Element semestre: - if code in {x.strip() for x in sem["elt_sem_apo"].split(",")}: - if self.export_res_sem: - return self.comp_elt_semestre(nt, decision, etudid) - else: - return VOID_APO_RES - # Elements UE - decisions_ue = nt.get_etud_decisions_ue(etudid) - for ue in nt.get_ues_stat_dict(): + decisions_ue = res.get_etud_decisions_ue(etudid) + for ue in res.get_ues_stat_dict(): if ue["code_apogee"] and code in { x.strip() for x in ue["code_apogee"].split(",") }: if self.export_res_ues: if decisions_ue and ue["ue_id"] in decisions_ue: - ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) + ue_status = res.get_etud_ue_status(etudid, ue["ue_id"]) code_decision_ue = decisions_ue[ue["ue_id"]]["code"] return dict( N=self.fmt_note(ue_status["moy"] if ue_status else ""), @@ -459,14 +480,14 @@ class ApoEtud(dict): return VOID_APO_RES # Elements Modules - modimpls = nt.get_modimpls_dict() + modimpls = res.get_modimpls_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(",") }: - n = nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) + 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: @@ -480,8 +501,7 @@ class ApoEtud(dict): """Calcul résultat apo semestre. Toujours vide pour en BUT/APC. """ - if self.is_apc: - # pas de code semestre en APC ! + if self.is_apc: # garde fou: pas de code semestre en APC ! return dict(N="", B=20, J="", R="", M="") if decision is None: etud = Identite.get_etud(etudid) @@ -498,7 +518,7 @@ class ApoEtud(dict): note_str = self.fmt_note(note) return dict(N=note_str, B=20, J="", R=decision_apo, M="") - def comp_elt_annuel(self, etudid, cur_sem, autre_sem): + def comp_elt_annuel(self, etudid): """Calcul resultat annuel (VET) à partir du semestre courant et de l'autre (le suivant ou le précédent complétant l'année scolaire) En BUT, c'est la décision de jury annuelle (ApcValidationAnnee). @@ -516,18 +536,16 @@ class ApoEtud(dict): # XXX cette règle est discutable, à valider # log('comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id'])) - if not cur_sem: + if not self.cur_sem: # l'étudiant n'a pas de semestre courant ?! self.log.append("pas de semestre courant") log(f"comp_elt_annuel: etudid {etudid} has no cur_sem") return VOID_APO_RES - cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"]) - cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre) if self.is_apc: cur_decision = {} # comp_elt_semestre sera vide. else: - cur_decision = cur_nt.get_etud_decision_sem(etudid) + cur_decision = self.cur_res.get_etud_decision_sem(etudid) if not cur_decision: # pas de decision => pas de résultat annuel return VOID_APO_RES @@ -536,21 +554,17 @@ class ApoEtud(dict): # ne touche pas aux RATs return VOID_APO_RES - if not autre_sem: + if not self.autre_sem: # formations monosemestre, ou code VET semestriel, # ou jury intermediaire et etudiant non redoublant... - return self.comp_elt_semestre(cur_nt, cur_decision, etudid) + return self.comp_elt_semestre(self.cur_res, cur_decision, etudid) - autre_formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"]) - autre_nt: NotesTableCompat = res_sem.load_formsemestre_results( - autre_formsemestre - ) # --- Traite le BUT à part: if self.is_apc: - return self.comp_elt_annuel_apc(cur_nt, autre_nt, etudid) + return self.comp_elt_annuel_apc() # --- Formations classiques decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"]) - autre_decision = autre_nt.get_etud_decision_sem(etudid) + autre_decision = self.autre_res.get_etud_decision_sem(etudid) if not autre_decision: # pas de decision dans l'autre => pas de résultat annuel return VOID_APO_RES @@ -566,8 +580,8 @@ class ApoEtud(dict): ): note_str = "0,01" # note non nulle pour les démissionnaires else: - note = cur_nt.get_etud_moy_gen(etudid) - autre_note = autre_nt.get_etud_moy_gen(etudid) + note = self.cur_res.get_etud_moy_gen(etudid) + autre_note = self.autre_res.get_etud_moy_gen(etudid) # print 'note=%s autre_note=%s' % (note, autre_note) try: moy_annuelle = (note + autre_note) / 2 @@ -582,40 +596,46 @@ class ApoEtud(dict): return dict(N=note_str, B=20, J="", R=decision_apo_annuelle, M="") - def comp_elt_annuel_apc( - self, - cur_res: ResultatsSemestreBUT, - autre_res: ResultatsSemestreBUT, - etudid: int, - ): + def comp_elt_annuel_apc(self): """L'élément Apo pour un résultat annuel BUT. - cur_res : les résultats du semestre sur lequel a été appelé l'export. + self.cur_res == résultats du semestre sur lequel a été appelé l'export. """ + if not self.validation_annee_but: + # pas de décision ou pas de sem. impair + return VOID_APO_RES + + return dict( + N="", + B=20, + J="", + R=ScoDocSiteConfig.get_code_apo(self.validation_annee_but.code), + M="", + ) + + def _but_load_validation_annuelle(self): + "charge la validation de jury BUT annuelle" # le semestre impair de l'année scolaire - if cur_res.formsemestre.semestre_id % 2: - formsemestre = cur_res.formsemestre + if self.cur_res.formsemestre.semestre_id % 2: + formsemestre = self.cur_res.formsemestre elif ( - autre_res - and autre_res.formsemestre.annee_scolaire() - == cur_res.formsemestre.annee_scolaire() + self.autre_res + and self.autre_res.formsemestre.annee_scolaire() + == self.cur_res.formsemestre.annee_scolaire() ): - formsemestre = autre_res.formsemestre + formsemestre = self.autre_res.formsemestre assert formsemestre.semestre_id % 2 else: # ne trouve pas de semestre impair - return VOID_APO_RES - - validation: ApcValidationAnnee = ApcValidationAnnee.query.filter_by( - formsemestre_id=formsemestre.id, etudid=etudid - ).first() - if validation is None: - return VOID_APO_RES - return dict( - N="", B=20, J="", R=ScoDocSiteConfig.get_code_apo(validation.code), M="" + self.validation_annee_but = None + return + self.validation_annee_but: ApcValidationAnnee = ( + ApcValidationAnnee.query.filter_by( + formsemestre_id=formsemestre.id, etudid=self.etud["etudid"] + ).first() ) - def etud_semestres_de_etape(self, apo_data): - """ + def etud_set_semestres_de_etape(self, apo_data): + """Set .cur_sem and .autre_sem et charge les résultats. Lorsqu'on a une formation semestrialisée mais avec un code étape annuel, il faut considérer les deux semestres ((S1,S2) ou (S3,S4)) pour calculer le code annuel (VET ou VRT1A (voir elt_annee_apo)). @@ -623,7 +643,7 @@ class ApoEtud(dict): Pour les jurys intermediaires (janvier, S1 ou S3): (S2 ou S4) de la même étape lors d'une année précédente ? - Renvoie le semestre "courant" et l'autre semestre, ou None s'il n'y en a pas. + Set cur_sem: le semestre "courant" et autre_sem, ou None s'il n'y en a pas. """ # Cherche le semestre "courant": cur_sems = [ @@ -648,19 +668,29 @@ class ApoEtud(dict): cur_sem = None for sem in cur_sems: formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - has_decision = nt.etud_has_decision(self.etud["etudid"]) + res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + has_decision = res.etud_has_decision(self.etud["etudid"]) if has_decision: cur_sem = sem + self.cur_res = res break if cur_sem is None: cur_sem = cur_sems[0] # aucun avec décision, prend le plus recent + if res.formsemestre.id == cur_sem["formsemestre_id"]: + self.cur_res = res + else: + formsemestre = FormSemestre.query.get_or_404( + cur_sem["formsemestre_id"] + ) + self.cur_res = res_sem.load_formsemestre_results(formsemestre) + + self.cur_sem = cur_sem if apo_data.cur_semestre_id <= 0: - return ( - cur_sem, - None, - ) # "autre_sem" non pertinent pour sessions sans semestres + # "autre_sem" non pertinent pour sessions sans semestres: + self.autre_sem = None + self.autre_res = None + return if apo_data.jury_intermediaire: # jury de janvier # Le semestre suivant: exemple 2 si on est en jury de S1 @@ -678,7 +708,7 @@ class ApoEtud(dict): courant_annee_debut = apo_data.annee_scolaire + 1 courant_mois_debut = 1 # ou 2 (fev-jul) else: - raise ValueError("invalid pediode value !") # bug ? + raise ValueError("invalid periode value !") # bug ? courant_date_debut = "%d-%02d-01" % ( courant_annee_debut, courant_mois_debut, @@ -709,15 +739,24 @@ class ApoEtud(dict): autre_sem = None for sem in autres_sems: formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - decision = nt.get_etud_decision_sem(self.etud["etudid"]) - if decision: + res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + if res.is_apc: + has_decision = res.etud_has_decision(self.etud["etudid"]) + else: + has_decision = res.get_etud_decision_sem(self.etud["etudid"]) + if has_decision: autre_sem = sem break if autre_sem is None: autre_sem = autres_sems[0] # aucun avec decision, prend le plus recent - return cur_sem, autre_sem + self.autre_sem = autre_sem + # Charge les résultats: + if autre_sem: + formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"]) + self.autre_res = res_sem.load_formsemestre_results(formsemestre) + else: + self.autre_res = None class ApoData: @@ -911,8 +950,12 @@ class ApoData: return elts # { code apo : ApoElt } def apo_read_etuds(self, f) -> list[ApoEtud]: - """Lecture des etudiants (et resultats) du fichier CSV Apogée""" - L = [] + """Lecture des étudiants (et résultats) du fichier CSV Apogée. + Les lignes "étudiant" commencent toujours par + `12345678 NOM PRENOM 15/05/2003` + le premier code étant le NIP. + """ + apo_etuds = [] while True: line = f.readline() if not line: @@ -921,10 +964,15 @@ class ApoData: continue # silently ignore blank lines line = line.strip(APO_NEWLINE) fields = line.split(APO_SEP) + if len(fields) < 4: + raise ScoValueError( + """Ligne étudiant invalide + (doit commencer par 'NIP NOM PRENOM dd/mm/yyyy')""" + ) cols = {} # { col_id : value } for i, field in enumerate(fields): cols[self.col_ids[i]] = field - L.append( + apo_etuds.append( ApoEtud( nip=fields[0], # id etudiant nom=fields[1], @@ -940,7 +988,7 @@ class ApoData: ) ) - return L + return apo_etuds def get_etape_apogee(self): """Le code etape: 'V1RT', donné par le code de l'élément VET""" diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index e4cb58520..2a08d8624 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -87,12 +87,12 @@ class SemSet(dict): self.formsemestre_ids = [] cnx = ndb.GetDBConnexion() if semset_id: # read existing set - L = semset_list(cnx, args={"semset_id": semset_id}) - if not L: + semsets = semset_list(cnx, args={"semset_id": semset_id}) + if not semsets: raise ScoValueError(f"Ensemble inexistant ! (semset {semset_id})") - self["title"] = L[0]["title"] - self["annee_scolaire"] = L[0]["annee_scolaire"] - self["sem_id"] = L[0]["sem_id"] + self["title"] = semsets[0]["title"] + self["annee_scolaire"] = semsets[0]["annee_scolaire"] + self["sem_id"] = semsets[0]["sem_id"] r = ndb.SimpleDictFetch( "SELECT formsemestre_id FROM notes_semset_formsemestre WHERE semset_id = %(semset_id)s", {"semset_id": semset_id}, @@ -307,7 +307,7 @@ class SemSet(dict): if self["sem_id"] == 1: periode = "1re période (S1, S3)" - elif self["sem_id"] == 1: + elif self["sem_id"] == 2: periode = "2de période (S2, S4)" else: periode = "non semestrialisée (LP, ...). Incompatible avec BUT." @@ -441,8 +441,14 @@ def do_semset_add_sem(semset_id, formsemestre_id): if formsemestre_id == "": raise ScoValueError("pas de semestre choisi !") semset = SemSet(semset_id=semset_id) - semset.add(formsemestre.id) - return flask.redirect("apo_semset_maq_status?semset_id=%s" % semset_id) + semset.add(formsemestre_id) + return flask.redirect( + url_for( + "notes.apo_semset_maq_status", + scodoc_dept=g.scodoc_dept, + semset_id=semset_id, + ) + ) def do_semset_remove_sem(semset_id, formsemestre_id):