forked from ScoDoc/ScoDoc
1021 lines
38 KiB
Python
1021 lines
38 KiB
Python
# -*- 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.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.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.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())
|