# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """Semestres: gestion parcours DUT (Arreté du 13 août 2005) """ from app import db from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import ( FormSemestre, Identite, ScolarAutorisationInscription, Scolog, UniteEns, ) import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log from app.scodoc import sco_cache from app.scodoc import sco_formsemestre from app.scodoc.codes_cursus import ( CMP, ADC, ADJ, ADM, AJ, ATT, NO_SEMESTRE_ID, BUG, NEXT, NEXT2, NEXT_OR_NEXT2, REO, REDOANNEE, REDOSEM, RA_OR_NEXT, RA_OR_RS, RS_OR_NEXT, CODES_SEM_VALIDES, NOTES_BARRE_GEN_COMPENSATION, code_semestre_attente, code_semestre_validant, ) from app.scodoc.dutrules import DUTRules # regles generees a partir du CSV from app.scodoc.sco_exceptions import ScoValueError class DecisionSem(object): "Decision prenable pour un semestre" def __init__( self, code_etat=None, code_etat_ues: dict = None, # { ue_id : code } new_code_prev="", explication="", # aide pour le jury formsemestre_id_utilise_pour_compenser=None, # None si code != ADC devenir=None, # code devenir assiduite=True, rule_id=None, # id regle correspondante ): self.code_etat = code_etat self.code_etat_ues = code_etat_ues or {} self.new_code_prev = new_code_prev self.explication = explication self.formsemestre_id_utilise_pour_compenser = ( formsemestre_id_utilise_pour_compenser ) self.devenir = devenir self.assiduite = assiduite self.rule_id = rule_id # code unique (string) utilise pour la gestion du formulaire self.codechoice = ( "C" # prefix pour éviter que Flask le considère comme int + str( hash( ( code_etat, new_code_prev, formsemestre_id_utilise_pour_compenser, devenir, assiduite, ) ) ) ) class SituationEtudCursus: "Semestre dans un cursus" class SituationEtudCursusClassic(SituationEtudCursus): "Semestre dans un parcours" def __init__(self, etud: Identite, formsemestre_id: int, nt: NotesTableCompat): """ etud: dict filled by fill_etuds_info() """ assert formsemestre_id == nt.formsemestre.id self.etud = etud self.etudid = etud.id self.formsemestre_id = formsemestre_id self.formsemestres: list[FormSemestre] = [] "les semestres parcourus, le plus ancien en tête" self.sem = sco_formsemestre.get_formsemestre( formsemestre_id ) # TODO utiliser formsemestres self.cur_sem: FormSemestre = nt.formsemestre self.can_compensate: set[int] = set() "les formsemestre_id qui peuvent compenser le courant" self.nt: NotesTableCompat = nt self.formation = self.nt.formsemestre.formation self.parcours = self.nt.parcours # Ce semestre est-il le dernier de la formation ? (e.g. semestre 4 du DUT) # pour le DUT, le dernier est toujours S4. # Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1 # (licences et autres formations en 1 seule session)) self.semestre_non_terminal = self.cur_sem.semestre_id != self.parcours.NB_SEM if self.cur_sem.semestre_id == NO_SEMESTRE_ID: self.semestre_non_terminal = False # Liste des semestres du parcours de cet étudiant: self._comp_semestres() # Determine le semestre "precedent" self._search_prev() # Verifie barres self._comp_barres() # Verifie compensation if self.prev_formsemestre and self.cur_sem.gestion_compensation: self.can_compensate_with_prev = ( self.prev_formsemestre.id in self.can_compensate ) else: self.can_compensate_with_prev = False def get_possible_choices(self, assiduite=True): """Donne la liste des décisions possibles en jury (hors décisions manuelles) (liste d'instances de DecisionSem) assiduite = True si pas de probleme d'assiduité """ choices = [] if self.prev_decision: prev_code_etat = self.prev_decision["code"] else: prev_code_etat = None state = ( prev_code_etat, assiduite, self.barre_moy_ok, self.barres_ue_ok, self.can_compensate_with_prev, self.semestre_non_terminal, ) # log('get_possible_choices: state=%s' % str(state) ) for rule in DUTRules: # Saute codes non autorisés dans ce parcours (eg ATT en LP) if rule.conclusion[0] in self.parcours.UNUSED_CODES: continue # Saute regles REDOSEM si pas de semestres decales: if (not self.cur_sem.gestion_semestrielle) and rule.conclusion[ 3 ] == "REDOSEM": continue if rule.match(state): if rule.conclusion[0] == ADC: # dans les regles on ne peut compenser qu'avec le PRECEDENT: fiduc = self.prev_formsemestre.id assert fiduc else: fiduc = None # Detection d'incoherences (regles BUG) if rule.conclusion[5] == BUG: log(f"get_possible_choices: inconsistency: state={state}") # # valid_semestre = code_semestre_validant(rule.conclusion[0]) choices.append( DecisionSem( code_etat=rule.conclusion[0], new_code_prev=rule.conclusion[2], devenir=rule.conclusion[3], formsemestre_id_utilise_pour_compenser=fiduc, explication=rule.conclusion[5], assiduite=assiduite, rule_id=rule.rule_id, ) ) return choices def explique_devenir(self, devenir): "Phrase d'explication pour le code devenir" if not devenir: return "" s_idx = self.cur_sem.semestre_id # numero semestre courant if s_idx < 0: # formation sans semestres (eg licence) next_s = 1 else: next_s = self._get_next_semestre_id() # log('s=%s next=%s' % (s, next_s)) sess_abrv = self.parcours.SESSION_ABBRV # 'S' ou 'A' if self.semestre_non_terminal and not self.all_other_validated(): passage = f"Passe en {sess_abrv}{next_s}" else: passage = "Formation terminée" if devenir == NEXT: return passage elif devenir == REO: return "Réorienté" elif devenir == REDOANNEE: return f"Redouble année (recommence {sess_abrv}{s_idx - 1})" elif devenir == REDOSEM: return f"Redouble semestre (recommence en {sess_abrv}{s_idx})" elif devenir == RA_OR_NEXT: return passage + ", ou redouble année (en {sess_abrv}{s_idx - 1})" elif devenir == RA_OR_RS: return f"""Redouble semestre {sess_abrv}{s_idx}, ou redouble année (en { sess_abrv}{s_idx - 1})""" elif devenir == RS_OR_NEXT: return f"{passage}, ou semestre {sess_abrv}{s_idx}" elif devenir == NEXT_OR_NEXT2: # coherent avec get_next_semestre_ids return f"{passage}, ou en semestre {sess_abrv}{s_idx + 2}" elif devenir == NEXT2: return f"Passe en {sess_abrv}{s_idx + 2}" else: log(f"explique_devenir: code devenir inconnu: {devenir}") return "Code devenir inconnu !" def all_other_validated(self): "True si tous les autres semestres de cette formation sont validés" return self._sems_validated(exclude_current=True) def sem_idx_is_validated(self, semestre_id): "True si le semestre d'indice indiqué est validé dans ce parcours" return self._sem_list_validated(set([semestre_id])) def parcours_validated(self): "True si parcours validé (diplôme obtenu, donc)." return self._sems_validated() def _sems_validated(self, exclude_current=False): "True si semestres du parcours validés" if self.cur_sem.semestre_id == NO_SEMESTRE_ID: # mono-semestre: juste celui ci decision = self.nt.get_etud_decision_sem(self.etudid) return decision and code_semestre_validant(decision["code"]) else: to_validate = set( range(1, self.parcours.NB_SEM + 1) ) # ensemble des indices à valider if exclude_current and self.cur_sem.semestre_id in to_validate: to_validate.remove(self.cur_sem.semestre_id) return self._sem_list_validated(to_validate) def can_jump_to_next2(self): """True si l'étudiant peut passer directement en Sn+2 (eg de S2 en S4). Il faut donc que tous les semestres 1...n-1 soient validés et que n+1 soit en attente. (et que le sem courant n soit validé, ce qui n'est pas testé ici) """ s_idx = self.cur_sem.semestre_id if not self.cur_sem.gestion_semestrielle: return False # pas de semestre décalés if s_idx == NO_SEMESTRE_ID or s_idx > self.parcours.NB_SEM - 2: return False # n+2 en dehors du parcours if self._sem_list_validated(set(range(1, s_idx))): # antérieurs validés, teste suivant n1 = s_idx + 1 for formsemestre in self.formsemestres: if ( formsemestre.semestre_id == n1 and formsemestre.formation.formation_code == self.formation.formation_code ): nt: NotesTableCompat = res_sem.load_formsemestre_results( formsemestre ) decision = nt.get_etud_decision_sem(self.etudid) if decision and ( code_semestre_validant(decision["code"]) or code_semestre_attente(decision["code"]) ): return True return False def _sem_list_validated(self, sem_idx_set): """True si les semestres dont les indices sont donnés en argument (modifié) sont validés. En sortie, sem_idx_set contient ceux qui n'ont pas été validés.""" for sem in self.get_semestres(): if sem["formation_code"] == self.formation.formation_code: formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) decision = nt.get_etud_decision_sem(self.etudid) if decision and code_semestre_validant(decision["code"]): # validé sem_idx_set.discard(sem["semestre_id"]) return not sem_idx_set def _comp_semestres(self): # plus ancien en tête: self.formsemestres = self.etud.get_formsemestres(recent_first=False) # Nb max d'UE et acronymes ue_acros = {} # acronyme ue : 1 nb_max_ue = 0 sems = [] for formsemestre in self.formsemestres: # plus ancien en tête nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) sem = formsemestre.to_dict() sems.append(sem) ues = nt.get_ues_stat_dict(filter_sport=True) for ue in ues: ue_acros[ue["acronyme"]] = 1 nb_ue = len(ues) if nb_ue > nb_max_ue: nb_max_ue = nb_ue # add formation_code to each sem: sem["formation_code"] = formsemestre.formation.formation_code # si sem peut servir à compenser le semestre courant, positionne # can_compensate if self.check_compensation_dut(sem, nt): self.can_compensate.add(formsemestre.id) self.ue_acros = list(ue_acros.keys()) self.ue_acros.sort() self.nb_max_ue = nb_max_ue self.sems = sems def get_semestres(self) -> list[dict]: """Liste des semestres dans lesquels a été inscrit l'étudiant (quelle que soit la formation), le plus ancien en tête""" return self.sems def get_cursus_descr(self, filter_futur=False, filter_formation_code=False) -> str: """Description brève du parcours: "S1, S2, ..." Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant. Si filter_formation_code, restreint aux semestres de même code formation que le courant. """ cur_begin_date = self.cur_sem.date_debut cur_formation_code = self.cur_sem.formation.formation_code p = [] for formsemestre in self.formsemestres: inscription = formsemestre.etuds_inscriptions.get(self.etud.id) if inscription is None: return "non inscrit" # !!! if inscription.etat == scu.DEMISSION: dem = " (dem.)" else: dem = "" if filter_futur and formsemestre.date_debut > cur_begin_date: continue # skip semestres demarrant apres le courant if ( filter_formation_code and formsemestre.formation.formation_code != cur_formation_code ): continue # restreint aux semestres de la formation courante (pour les PV) session_abbrv = self.parcours.SESSION_ABBRV # 'S' ou 'A' if formsemestre.semestre_id < 0: session_abbrv = "A" # force, cas des DUT annuels par exemple p.append("%s%d%s" % (session_abbrv, -formsemestre.semestre_id, dem)) else: p.append("%s%d%s" % (session_abbrv, formsemestre.semestre_id, dem)) return ", ".join(p) def get_parcours_decisions(self): """Decisions de jury de chacun des semestres du parcours, du S1 au NB_SEM+1, ou mono-semestre. Returns: { semestre_id : code } """ r = {} if self.cur_sem.semestre_id == NO_SEMESTRE_ID: indices = [NO_SEMESTRE_ID] else: indices = list(range(1, self.parcours.NB_SEM + 1)) for i in indices: # cherche dans les semestres de l'étudiant, en partant du plus récent sem = None for asem in reversed(self.get_semestres()): if asem["semestre_id"] == i: sem = asem break if not sem: code = "" # non inscrit à ce semestre else: formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) decision = nt.get_etud_decision_sem(self.etudid) if decision: code = decision["code"] else: code = "-" r[i] = code return r def _comp_barres(self): "calcule barres_ue_ok et barre_moy_ok: barre moy. gen. et barres UE" self.barres_ue_ok, self.barres_ue_diag = self.nt.etud_check_conditions_ues( self.etudid ) self.moy_gen = self.nt.get_etud_moy_gen(self.etudid) self.barre_moy_ok = (isinstance(self.moy_gen, float)) and ( self.moy_gen >= (self.parcours.BARRE_MOY - scu.NOTES_TOLERANCE) ) # conserve etat UEs ue_ids = [x["ue_id"] for x in self.nt.get_ues_stat_dict(filter_sport=True)] self.ues_status = {} # ue_id : status for ue_id in ue_ids: self.ues_status[ue_id] = self.nt.get_etud_ue_status(self.etudid, ue_id) def could_be_compensated(self): "true si ce semestre pourrait etre compensé par un autre (e.g. barres UE > 8)" return self.barres_ue_ok def _search_prev(self) -> FormSemestre | None: """Recherche semestre 'precedent'. positionne .prev_decision """ self.prev_formsemestre = None self.prev_decision = None if len(self.formsemestres) < 2: return None # Cherche sem courant dans la liste triee par date_debut cur = None icur = -1 for cur in self.formsemestres: icur += 1 if cur.id == self.formsemestre_id: break if not cur or cur.id != self.formsemestre_id: log( f"""*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={ self.formsemestre_id}, etudid={self.etudid})""" ) return None # pas de semestre courant !!! # Cherche semestre antérieur de même formation (code) et semestre_id precedent # # i = icur - 1 # part du courant, remonte vers le passé i = len(self.formsemestres) - 1 # par du dernier, remonte vers le passé prev_formsemestre = None while i >= 0: if ( self.formsemestres[i].formation.formation_code == self.formation.formation_code and self.formsemestres[i].semestre_id == cur.semestre_id - 1 ): prev_formsemestre = self.formsemestres[i] break i -= 1 if not prev_formsemestre: return None # pas de precedent trouvé self.prev_formsemestre = prev_formsemestre # Verifications basiques: # ? # Code etat du semestre precedent: nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_formsemestre) self.prev_decision = nt.get_etud_decision_sem(self.etudid) self.prev_moy_gen = nt.get_etud_moy_gen(self.etudid) self.prev_barres_ue_ok = nt.etud_check_conditions_ues(self.etudid)[0] def get_next_semestre_ids(self, devenir: str) -> list[int]: """Liste des numeros de semestres autorises avec ce devenir Ne vérifie pas que le devenir est possible (doit être fait avant), juste que le rang du semestre est dans le parcours [1..NB_SEM] """ s_idx = self.cur_sem.semestre_id if devenir == NEXT: ids = [self._get_next_semestre_id()] elif devenir == REDOANNEE: ids = [s_idx - 1] elif devenir == REDOSEM: ids = [s_idx] elif devenir == RA_OR_NEXT: ids = [s_idx - 1, self._get_next_semestre_id()] elif devenir == RA_OR_RS: ids = [s_idx - 1, s_idx] elif devenir == RS_OR_NEXT: ids = [s_idx, self._get_next_semestre_id()] elif devenir == NEXT_OR_NEXT2: ids = [ self._get_next_semestre_id(), s_idx + 2, ] # cohérent avec explique_devenir() elif devenir == NEXT2: ids = [s_idx + 2] else: ids = [] # reoriente ou autre: pas de next ! # clip [1..NB_SEM] r = [] for idx in ids: if 0 < idx <= self.parcours.NB_SEM: r.append(idx) return r def _get_next_semestre_id(self): """Indice du semestre suivant non validé. S'il n'y en a pas, ramène NB_SEM+1 """ s_idx = self.cur_sem.semestre_id if s_idx >= self.parcours.NB_SEM: return self.parcours.NB_SEM + 1 validated = True while validated and (s_idx < self.parcours.NB_SEM): s_idx = s_idx + 1 # semestre s validé ? validated = False for formsemestre in self.formsemestres: if ( formsemestre.formation.formation_code == self.formation.formation_code and formsemestre.semestre_id == s_idx ): nt: NotesTableCompat = res_sem.load_formsemestre_results( formsemestre ) decision = nt.get_etud_decision_sem(self.etudid) if decision and code_semestre_validant(decision["code"]): validated = True return s_idx def valide_decision(self, decision): """Enregistre la decision (instance de DecisionSem) Enregistre codes semestre et UE, et autorisations inscription. """ cnx = ndb.GetDBConnexion() # -- check if decision.code_etat in self.parcours.UNUSED_CODES: raise ScoValueError("code decision invalide dans ce parcours") # if decision.code_etat == ADC: fsid = decision.formsemestre_id_utilise_pour_compenser if fsid: ok = False for formsemestre in self.formsemestres: if ( formsemestre.id == fsid and formsemestre.id in self.can_compensate ): ok = True break if not ok: raise ScoValueError("valide_decision: compensation impossible") # -- supprime decision precedente et enregistre decision to_invalidate = [] if self.nt.get_etud_decision_sem(self.etudid): to_invalidate = formsemestre_update_validation_sem( cnx, self.formsemestre_id, self.etudid, decision.code_etat, decision.assiduite, decision.formsemestre_id_utilise_pour_compenser, ) else: formsemestre_validate_sem( cnx, self.formsemestre_id, self.etudid, decision.code_etat, decision.assiduite, decision.formsemestre_id_utilise_pour_compenser, ) Scolog.logdb( method="validate_sem", etudid=self.etudid, commit=False, msg=f"formsemestre_id={self.formsemestre_id} code={decision.code_etat}", ) # -- decisions UEs formsemestre_validate_ues( self.formsemestre_id, self.etudid, decision.code_etat, decision.assiduite, ) # -- modification du code du semestre precedent if self.prev_formsemestre and decision.new_code_prev: if decision.new_code_prev == ADC: # ne compense le prec. qu'avec le sem. courant fsid = self.formsemestre_id else: fsid = None to_invalidate += formsemestre_update_validation_sem( cnx, self.prev_formsemestre.id, self.etudid, decision.new_code_prev, assidu=True, formsemestre_id_utilise_pour_compenser=fsid, ) Scolog.logdb( method="validate_sem", etudid=self.etudid, commit=False, msg=f"formsemestre_id={self.prev_formsemestre.id} code={decision.new_code_prev}", ) # modifs des codes d'UE (pourraient passer de ADM a CMP, meme sans modif des notes) formsemestre_validate_ues( self.prev_formsemestre.id, self.etudid, decision.new_code_prev, decision.assiduite, # attention: en toute rigueur il faudrait utiliser # une indication de l'assiduite au sem. precedent, que nous n'avons pas... ) sco_cache.invalidate_formsemestre( formsemestre_id=self.prev_formsemestre.id ) # > modif decisions jury (sem, UE) try: # -- Supprime autorisations venant de ce formsemestre autorisations = ScolarAutorisationInscription.query.filter_by( etudid=self.etudid, origin_formsemestre_id=self.formsemestre_id ) for autorisation in autorisations: db.session.delete(autorisation) db.session.flush() # -- Enregistre autorisations inscription next_semestre_ids = self.get_next_semestre_ids(decision.devenir) for next_semestre_id in next_semestre_ids: autorisation = ScolarAutorisationInscription( etudid=self.etudid, formation_code=self.formation.formation_code, semestre_id=next_semestre_id, origin_formsemestre_id=self.formsemestre_id, ) db.session.add(autorisation) db.session.commit() except: cnx.session.rollback() raise sco_cache.invalidate_formsemestre( formsemestre_id=self.formsemestre_id ) # > modif decisions jury et autorisations inscription if decision.formsemestre_id_utilise_pour_compenser: # inval aussi le semestre utilisé pour compenser: sco_cache.invalidate_formsemestre( formsemestre_id=decision.formsemestre_id_utilise_pour_compenser, ) # > modif decision jury for formsemestre_id in to_invalidate: sco_cache.invalidate_formsemestre( formsemestre_id=formsemestre_id ) # > modif decision jury def check_compensation_dut(self, semc: dict, ntc: NotesTableCompat): """Compensations DUT Vérifie si le semestre sem peut se compenser en utilisant semc - semc non utilisé par un autre semestre - decision du jury prise ADM ou ADJ ou ATT ou ADC - barres UE (moy ue > 8) dans sem et semc - moyenne des moy_gen > 10 Return boolean """ # -- deja utilise ? decc = ntc.get_etud_decision_sem(self.etudid) if ( decc and decc["compense_formsemestre_id"] and decc["compense_formsemestre_id"] != self.sem["formsemestre_id"] ): return False # -- semestres consecutifs ? if abs(self.sem["semestre_id"] - semc["semestre_id"]) != 1: return False # -- decision jury: if decc and not decc["code"] in (ADM, ADJ, ATT, ADC): return False # -- barres UE et moyenne des moyennes: moy_gen = self.nt.get_etud_moy_gen(self.etudid) moy_genc = ntc.get_etud_moy_gen(self.etudid) try: moy_moy = (moy_gen + moy_genc) / 2 except: # un des semestres sans aucune note ! return False if ( self.nt.etud_check_conditions_ues(self.etudid)[0] and ntc.etud_check_conditions_ues(self.etudid)[0] and moy_moy >= NOTES_BARRE_GEN_COMPENSATION ): return True else: return False class SituationEtudCursusECTS(SituationEtudCursusClassic): """Gestion parcours basés sur ECTS""" def __init__(self, etud: Identite, formsemestre_id: int, nt): SituationEtudCursusClassic.__init__(self, etud, formsemestre_id, nt) def could_be_compensated(self): return False # jamais de compensations dans ce parcours def get_possible_choices(self, assiduite=True): """Listes de décisions "recommandées" (hors décisions manuelles) Dans ce type de parcours, on n'utilise que ADM, AJ, et ADJ (?). """ etud_ects_infos = self.nt.get_etud_ects_pot(self.etudid) if ( etud_ects_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR and etud_ects_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR ): choices = [ DecisionSem( code_etat=ADM, new_code_prev=None, devenir=NEXT, formsemestre_id_utilise_pour_compenser=None, explication="Semestre validé", assiduite=assiduite, rule_id="1000", ) ] else: choices = [ DecisionSem( code_etat=AJ, new_code_prev=None, devenir=NEXT, formsemestre_id_utilise_pour_compenser=None, explication="Semestre non validé", assiduite=assiduite, rule_id="1001", ) ] return choices # ------------------------------------------------------------------------------------------- _scolar_formsemestre_validation_editor = ndb.EditableTable( "scolar_formsemestre_validation", "formsemestre_validation_id", ( "formsemestre_validation_id", "etudid", "formsemestre_id", "ue_id", "code", "assidu", "event_date", "compense_formsemestre_id", "moy_ue", "semestre_id", "is_external", ), output_formators={ "event_date": ndb.DateISOtoDMY, }, input_formators={ "event_date": ndb.DateDMYtoISO, "assidu": bool, "is_external": bool, }, ) scolar_formsemestre_validation_create = _scolar_formsemestre_validation_editor.create scolar_formsemestre_validation_list = _scolar_formsemestre_validation_editor.list scolar_formsemestre_validation_delete = _scolar_formsemestre_validation_editor.delete scolar_formsemestre_validation_edit = _scolar_formsemestre_validation_editor.edit def formsemestre_validate_sem( cnx, formsemestre_id, etudid, code, assidu=True, formsemestre_id_utilise_pour_compenser=None, ): "Ajoute ou change validation semestre" args = {"formsemestre_id": formsemestre_id, "etudid": etudid} # delete existing cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) try: cursor.execute( """delete from scolar_formsemestre_validation where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s and ue_id is null""", args, ) # insert args["code"] = code args["assidu"] = assidu log("formsemestre_validate_sem: %s" % args) scolar_formsemestre_validation_create(cnx, args) # marque sem. utilise pour compenser: if formsemestre_id_utilise_pour_compenser: assert code == ADC args2 = { "formsemestre_id": formsemestre_id_utilise_pour_compenser, "compense_formsemestre_id": formsemestre_id, "etudid": etudid, } cursor.execute( """update scolar_formsemestre_validation set compense_formsemestre_id=%(compense_formsemestre_id)s where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s and ue_id is null""", args2, ) except: cnx.rollback() raise def formsemestre_update_validation_sem( cnx, formsemestre_id, etudid, code, assidu=True, formsemestre_id_utilise_pour_compenser=None, ): "Update validation semestre" args = { "formsemestre_id": formsemestre_id, "etudid": etudid, "code": code, "assidu": assidu, } log("formsemestre_update_validation_sem: %s" % args) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) to_invalidate = [] # enleve compensations si necessaire # recupere les semestres auparavant utilisés pour invalider les caches # correspondants: cursor.execute( """select formsemestre_id from scolar_formsemestre_validation where compense_formsemestre_id=%(formsemestre_id)s and etudid = %(etudid)s""", args, ) to_invalidate = [x[0] for x in cursor.fetchall()] # suppress: cursor.execute( """update scolar_formsemestre_validation set compense_formsemestre_id=NULL where compense_formsemestre_id=%(formsemestre_id)s and etudid = %(etudid)s""", args, ) if formsemestre_id_utilise_pour_compenser: assert code == ADC # marque sem. utilise pour compenser: args2 = { "formsemestre_id": formsemestre_id_utilise_pour_compenser, "compense_formsemestre_id": formsemestre_id, "etudid": etudid, } cursor.execute( """update scolar_formsemestre_validation set compense_formsemestre_id=%(compense_formsemestre_id)s where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s and ue_id is null""", args2, ) cursor.execute( """update scolar_formsemestre_validation set code = %(code)s, event_date=DEFAULT, assidu=%(assidu)s where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s and ue_id is null""", args, ) return to_invalidate def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite): """Enregistre codes UE, selon état semestre. Les codes UE sont toujours calculés ici, et non passés en paramètres car ils ne dépendent que de la note d'UE et de la validation ou non du semestre. Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ). """ valid_semestre = code_etat_sem in CODES_SEM_VALIDES cnx = ndb.GetDBConnexion() formsemestre = FormSemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) ue_ids = [x["ue_id"] for x in nt.get_ues_stat_dict(filter_sport=True)] for ue_id in ue_ids: ue_status = nt.get_etud_ue_status(etudid, ue_id) if not assiduite: code_ue = AJ else: # log('%s: %s: ue_status=%s' % (formsemestre_id,ue_id,ue_status)) if ( isinstance(ue_status["moy"], float) and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE ): code_ue = ADM elif not isinstance(ue_status["moy"], float): # aucune note (pas de moyenne) dans l'UE: ne la valide pas code_ue = None elif valid_semestre: code_ue = CMP else: code_ue = AJ # log('code_ue=%s' % code_ue) if etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id) and code_ue: do_formsemestre_validate_ue( cnx, nt, formsemestre_id, etudid, ue_id, code_ue ) Scolog.logdb( method="validate_ue", etudid=etudid, msg=f"ue_id={ue_id} code={code_ue}", commit=False, ) db.session.commit() cnx.commit() def do_formsemestre_validate_ue( cnx, nt, formsemestre_id, etudid, ue_id, code, moy_ue=None, date=None, semestre_id=None, is_external=False, ): """Ajoute ou change validation UE""" if semestre_id is None: ue = UniteEns.query.get_or_404(ue_id) semestre_id = ue.semestre_idx args = { "formsemestre_id": formsemestre_id, "etudid": etudid, "ue_id": ue_id, "semestre_id": semestre_id, "is_external": is_external, "moy_ue": moy_ue, } if date: args["event_date"] = date # delete existing cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) try: cond = "etudid = %(etudid)s and ue_id=%(ue_id)s" if formsemestre_id: cond += " and formsemestre_id=%(formsemestre_id)s" if semestre_id: cond += " and (semestre_id=%(semestre_id)s or semestre_id is NULL)" log(f"formsemestre_validate_ue: deleting where {cond}, args={args})") cursor.execute("delete from scolar_formsemestre_validation where " + cond, args) # insert args["code"] = code if (code == ADM) and (moy_ue is None): # stocke la moyenne d'UE capitalisée: ue_status = nt.get_etud_ue_status(etudid, ue_id) args["moy_ue"] = ue_status["moy"] if ue_status else "" log("formsemestre_validate_ue: create %s" % args) if code is not None: scolar_formsemestre_validation_create(cnx, args) else: log("formsemestre_validate_ue: code is None, not recording validation") except: cnx.rollback() raise def formsemestre_has_decisions(formsemestre_id): """True s'il y a au moins une validation (decision de jury) dans ce semestre equivalent to notes_table.sem_has_decisions() but much faster when nt not cached """ cnx = ndb.GetDBConnexion() validations = scolar_formsemestre_validation_list( cnx, args={"formsemestre_id": formsemestre_id, "is_external": False} ) return len(validations) > 0 def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id): """Vrai si l'étudiant est inscrit à au moins un module de cette UE dans ce semestre. Ne pas utiliser pour les formations APC ! """ cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( """SELECT mi.* FROM notes_moduleimpl mi, notes_modules mo, notes_ue ue, notes_moduleimpl_inscription i WHERE i.etudid = %(etudid)s and i.moduleimpl_id=mi.id and mi.formsemestre_id = %(formsemestre_id)s and mi.module_id = mo.id and mo.ue_id = %(ue_id)s """, {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id}, ) return len(cursor.fetchall())