forked from ScoDoc/ScoDoc
1130 lines
41 KiB
Python
1130 lines
41 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2022 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.comp import res_sem
|
|
from app.comp.res_compat import NotesTableCompat
|
|
from app.models import FormSemestre, UniteEns
|
|
|
|
import app.scodoc.sco_utils as scu
|
|
import app.scodoc.notesdb as ndb
|
|
from app import log
|
|
from app.scodoc.scolog import logdb
|
|
from app.scodoc import sco_cache, sco_etud
|
|
from app.scodoc import sco_formsemestre
|
|
from app.scodoc import sco_formations
|
|
from app.scodoc.sco_codes_parcours 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={}, # { 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
|
|
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,
|
|
)
|
|
)
|
|
)
|
|
)
|
|
# xxx debug
|
|
# log('%s: %s %s %s %s %s' % (self.codechoice,code_etat,new_code_prev,formsemestre_id_utilise_pour_compenser,devenir,assiduite) )
|
|
|
|
|
|
def SituationEtudParcours(etud, formsemestre_id):
|
|
"""renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)"""
|
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
parcours = nt.parcours
|
|
#
|
|
if parcours.ECTS_ONLY:
|
|
return SituationEtudParcoursECTS(etud, formsemestre_id, nt)
|
|
else:
|
|
return SituationEtudParcoursGeneric(etud, formsemestre_id, nt)
|
|
|
|
|
|
class SituationEtudParcoursGeneric(object):
|
|
"Semestre dans un parcours"
|
|
|
|
def __init__(self, etud, formsemestre_id, nt):
|
|
"""
|
|
etud: dict filled by fill_etuds_info()
|
|
"""
|
|
self.etud = etud
|
|
self.etudid = etud["etudid"]
|
|
self.formsemestre_id = formsemestre_id
|
|
self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
self.nt = 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.sem["semestre_id"] != self.parcours.NB_SEM
|
|
if self.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.prev_formsemestre_id = self._search_prev()
|
|
# Verifie barres
|
|
self._comp_barres()
|
|
# Verifie compensation
|
|
if self.prev and self.sem["gestion_compensation"]:
|
|
self.can_compensate_with_prev = self.prev["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.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("get_possible_choices: inconsistency: state=%s" % str(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 = self.sem["semestre_id"] # numero semestre courant
|
|
if s < 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))
|
|
SA = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
|
if self.semestre_non_terminal and not self.all_other_validated():
|
|
passage = "Passe en %s%s" % (SA, next_s)
|
|
else:
|
|
passage = "Formation terminée"
|
|
if devenir == NEXT:
|
|
return passage
|
|
elif devenir == REO:
|
|
return "Réorienté"
|
|
elif devenir == REDOANNEE:
|
|
return "Redouble année (recommence %s%s)" % (SA, (s - 1))
|
|
elif devenir == REDOSEM:
|
|
return "Redouble semestre (recommence en %s%s)" % (SA, s)
|
|
elif devenir == RA_OR_NEXT:
|
|
return passage + ", ou redouble année (en %s%s)" % (SA, (s - 1))
|
|
elif devenir == RA_OR_RS:
|
|
return "Redouble semestre %s%s, ou redouble année (en %s%s)" % (
|
|
SA,
|
|
s,
|
|
SA,
|
|
s - 1,
|
|
)
|
|
elif devenir == RS_OR_NEXT:
|
|
return passage + ", ou semestre %s%s" % (SA, s)
|
|
elif devenir == NEXT_OR_NEXT2:
|
|
return passage + ", ou en semestre %s%s" % (
|
|
SA,
|
|
s + 2,
|
|
) # coherent avec get_next_semestre_ids
|
|
elif devenir == NEXT2:
|
|
return "Passe en %s%s" % (SA, s + 2)
|
|
else:
|
|
log("explique_devenir: code devenir inconnu: %s" % 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.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.sem["semestre_id"] in to_validate:
|
|
to_validate.remove(self.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)
|
|
"""
|
|
n = self.sem["semestre_id"]
|
|
if not self.sem["gestion_semestrielle"]:
|
|
return False # pas de semestre décalés
|
|
if n == NO_SEMESTRE_ID or n > self.parcours.NB_SEM - 2:
|
|
return False # n+2 en dehors du parcours
|
|
if self._sem_list_validated(set(range(1, n))):
|
|
# antérieurs validé, teste suivant
|
|
n1 = n + 1
|
|
for sem in self.get_semestres():
|
|
if (
|
|
sem["semestre_id"] == n1
|
|
and 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"])
|
|
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):
|
|
# etud['sems'] est trie par date decroissante (voir fill_etuds_info)
|
|
if not "sems" in self.etud:
|
|
self.etud["sems"] = sco_etud.etud_inscriptions_infos(
|
|
self.etud["etudid"], self.etud["ne"]
|
|
)["sems"]
|
|
sems = self.etud["sems"][:] # copy
|
|
sems.reverse()
|
|
# Nb max d'UE et acronymes
|
|
ue_acros = {} # acronyme ue : 1
|
|
nb_max_ue = 0
|
|
for sem in sems:
|
|
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
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"] = sco_formations.formation_list(
|
|
args={"formation_id": sem["formation_id"]}
|
|
)[0]["formation_code"]
|
|
# si sem peut servir à compenser le semestre courant, positionne
|
|
# can_compensate
|
|
sem["can_compensate"] = check_compensation(
|
|
self.etudid, self.sem, self.nt, sem, nt
|
|
)
|
|
|
|
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):
|
|
"""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_parcours_descr(self, filter_futur=False):
|
|
"""Description brève du parcours: "S1, S2, ..."
|
|
Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant.
|
|
"""
|
|
cur_begin_date = self.sem["dateord"]
|
|
p = []
|
|
for s in self.sems:
|
|
if s["ins"]["etat"] == "D":
|
|
dem = " (dem.)"
|
|
else:
|
|
dem = ""
|
|
if filter_futur and s["dateord"] > cur_begin_date:
|
|
continue # skip semestres demarrant apres le courant
|
|
SA = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
|
if s["semestre_id"] < 0:
|
|
SA = "A" # force, cas des DUT annuels par exemple
|
|
p.append("%s%d%s" % (SA, -s["semestre_id"], dem))
|
|
else:
|
|
p.append("%s%d%s" % (SA, s["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.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):
|
|
"""Recherche semestre 'precedent'.
|
|
return prev_formsemestre_id
|
|
"""
|
|
self.prev = None
|
|
self.prev_decision = None
|
|
if len(self.sems) < 2:
|
|
return None
|
|
# Cherche sem courant dans la liste triee par date_debut
|
|
cur = None
|
|
icur = -1
|
|
for cur in self.sems:
|
|
icur += 1
|
|
if cur["formsemestre_id"] == self.formsemestre_id:
|
|
break
|
|
if not cur or cur["formsemestre_id"] != self.formsemestre_id:
|
|
log(
|
|
"*** SituationEtudParcours: search_prev: cur not found (formsemestre_id=%s, etudid=%s)"
|
|
% (self.formsemestre_id, 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.sems) - 1 # par du dernier, remonte vers le passé
|
|
prev = None
|
|
while i >= 0:
|
|
if (
|
|
self.sems[i]["formation_code"] == self.formation.formation_code
|
|
and self.sems[i]["semestre_id"] == cur["semestre_id"] - 1
|
|
):
|
|
prev = self.sems[i]
|
|
break
|
|
i -= 1
|
|
if not prev:
|
|
return None # pas de precedent trouvé
|
|
self.prev = prev
|
|
# Verifications basiques:
|
|
# ?
|
|
# Code etat du semestre precedent:
|
|
formsemestre = FormSemestre.query.get_or_404(prev["formsemestre_id"])
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(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]
|
|
return self.prev["formsemestre_id"]
|
|
|
|
def get_next_semestre_ids(self, devenir):
|
|
"""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 = self.sem["semestre_id"]
|
|
if devenir == NEXT:
|
|
ids = [self._get_next_semestre_id()]
|
|
elif devenir == REDOANNEE:
|
|
ids = [s - 1]
|
|
elif devenir == REDOSEM:
|
|
ids = [s]
|
|
elif devenir == RA_OR_NEXT:
|
|
ids = [s - 1, self._get_next_semestre_id()]
|
|
elif devenir == RA_OR_RS:
|
|
ids = [s - 1, s]
|
|
elif devenir == RS_OR_NEXT:
|
|
ids = [s, self._get_next_semestre_id()]
|
|
elif devenir == NEXT_OR_NEXT2:
|
|
ids = [
|
|
self._get_next_semestre_id(),
|
|
s + 2,
|
|
] # cohérent avec explique_devenir()
|
|
elif devenir == NEXT2:
|
|
ids = [s + 2]
|
|
else:
|
|
ids = [] # reoriente ou autre: pas de next !
|
|
# clip [1..NB_SEM]
|
|
r = []
|
|
for idx in ids:
|
|
if idx > 0 and 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 = self.sem["semestre_id"]
|
|
if s >= self.parcours.NB_SEM:
|
|
return self.parcours.NB_SEM + 1
|
|
validated = True
|
|
while validated and (s < self.parcours.NB_SEM):
|
|
s = s + 1
|
|
# semestre s validé ?
|
|
validated = False
|
|
for sem in self.sems:
|
|
if (
|
|
sem["formation_code"] == self.formation.formation_code
|
|
and sem["semestre_id"] == s
|
|
):
|
|
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"]):
|
|
validated = True
|
|
return s
|
|
|
|
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 sem in self.sems:
|
|
if sem["formsemestre_id"] == fsid and sem["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,
|
|
)
|
|
logdb(
|
|
cnx,
|
|
method="validate_sem",
|
|
etudid=self.etudid,
|
|
commit=False,
|
|
msg="formsemestre_id=%s code=%s"
|
|
% (self.formsemestre_id, 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 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,
|
|
)
|
|
logdb(
|
|
cnx,
|
|
method="validate_sem",
|
|
etudid=self.etudid,
|
|
commit=False,
|
|
msg="formsemestre_id=%s code=%s"
|
|
% (self.prev["formsemestre_id"], 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)
|
|
|
|
# -- supprime autorisations venant de ce formsemestre
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
|
try:
|
|
cursor.execute(
|
|
"""delete from scolar_autorisation_inscription
|
|
where etudid = %(etudid)s and origin_formsemestre_id=%(origin_formsemestre_id)s
|
|
""",
|
|
{"etudid": self.etudid, "origin_formsemestre_id": self.formsemestre_id},
|
|
)
|
|
|
|
# -- enregistre autorisations inscription
|
|
next_semestre_ids = self.get_next_semestre_ids(decision.devenir)
|
|
for next_semestre_id in next_semestre_ids:
|
|
_scolar_autorisation_inscription_editor.create(
|
|
cnx,
|
|
{
|
|
"etudid": self.etudid,
|
|
"formation_code": self.formation.formation_code,
|
|
"semestre_id": next_semestre_id,
|
|
"origin_formsemestre_id": self.formsemestre_id,
|
|
},
|
|
)
|
|
cnx.commit()
|
|
except:
|
|
cnx.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
|
|
|
|
|
|
class SituationEtudParcoursECTS(SituationEtudParcoursGeneric):
|
|
"""Gestion parcours basés sur ECTS"""
|
|
|
|
def __init__(self, etud, formsemestre_id, nt):
|
|
SituationEtudParcoursGeneric.__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
|
|
|
|
|
|
#
|
|
def check_compensation(etudid, sem, nt, semc, ntc):
|
|
"""Verifie 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(etudid)
|
|
if (
|
|
decc
|
|
and decc["compense_formsemestre_id"]
|
|
and decc["compense_formsemestre_id"] != sem["formsemestre_id"]
|
|
):
|
|
return False
|
|
# -- semestres consecutifs ?
|
|
if abs(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 = nt.get_etud_moy_gen(etudid)
|
|
moy_genc = ntc.get_etud_moy_gen(etudid)
|
|
try:
|
|
moy_moy = (moy_gen + moy_genc) / 2
|
|
except: # un des semestres sans aucune note !
|
|
return False
|
|
|
|
if (
|
|
nt.etud_check_conditions_ues(etudid)[0]
|
|
and ntc.etud_check_conditions_ues(etudid)[0]
|
|
and moy_moy >= NOTES_BARRE_GEN_COMPENSATION
|
|
):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
# -------------------------------------------------------------------------------------------
|
|
|
|
|
|
def int_or_null(s):
|
|
if s == "":
|
|
return None
|
|
else:
|
|
return int(s)
|
|
|
|
|
|
_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 = CODES_SEM_VALIDES.get(code_etat_sem, False)
|
|
cnx = ndb.GetDBConnexion()
|
|
formsemestre = FormSemestre.query.get_or_404(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
|
|
)
|
|
|
|
logdb(
|
|
cnx,
|
|
method="validate_ue",
|
|
etudid=etudid,
|
|
msg="ue_id=%s code=%s" % (ue_id, code_ue),
|
|
commit=False,
|
|
)
|
|
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,
|
|
}
|
|
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:
|
|
if moy_ue is None:
|
|
# stocke la moyenne d'UE capitalisée:
|
|
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
|
moy_ue = ue_status["moy"] if ue_status else ""
|
|
args["moy_ue"] = moy_ue
|
|
log("formsemestre_validate_ue: create %s" % args)
|
|
if code != 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}
|
|
)
|
|
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())
|
|
|
|
|
|
_scolar_autorisation_inscription_editor = ndb.EditableTable(
|
|
"scolar_autorisation_inscription",
|
|
"autorisation_inscription_id",
|
|
("etudid", "formation_code", "semestre_id", "date", "origin_formsemestre_id"),
|
|
output_formators={"date": ndb.DateISOtoDMY},
|
|
input_formators={"date": ndb.DateDMYtoISO},
|
|
)
|
|
scolar_autorisation_inscription_list = _scolar_autorisation_inscription_editor.list
|
|
|
|
|
|
def formsemestre_get_autorisation_inscription(etudid, origin_formsemestre_id):
|
|
"""Liste des autorisations d'inscription pour cet étudiant
|
|
émanant du semestre indiqué.
|
|
"""
|
|
cnx = ndb.GetDBConnexion()
|
|
return scolar_autorisation_inscription_list(
|
|
cnx, {"origin_formsemestre_id": origin_formsemestre_id, "etudid": etudid}
|
|
)
|
|
|
|
|
|
def formsemestre_get_etud_capitalisation(
|
|
formation_id: int, semestre_idx: int, date_debut, etudid: int
|
|
) -> list[dict]:
|
|
"""Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant.
|
|
|
|
Recherche dans les semestres de la même formation (code) avec le même
|
|
semestre_id et une date de début antérieure à celle du semestre mentionné.
|
|
Et aussi les UE externes validées.
|
|
|
|
Resultat: [ { 'formsemestre_id' :
|
|
'ue_id' : ue_id dans le semestre origine
|
|
'ue_code' :
|
|
'moy_ue' :
|
|
'event_date' :
|
|
'is_external'
|
|
} ]
|
|
"""
|
|
cnx = ndb.GetDBConnexion()
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
|
cursor.execute(
|
|
"""
|
|
SELECT DISTINCT SFV.*, ue.ue_code
|
|
FROM notes_ue ue, notes_formations nf,
|
|
notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
|
|
|
|
WHERE ue.formation_id = nf.id
|
|
and nf.formation_code = nf2.formation_code
|
|
and nf2.id=%(formation_id)s
|
|
|
|
and SFV.ue_id = ue.id
|
|
and SFV.code = 'ADM'
|
|
and SFV.etudid = %(etudid)s
|
|
|
|
and ( (sem.id = SFV.formsemestre_id
|
|
and sem.date_debut < %(date_debut)s
|
|
and sem.semestre_id = %(semestre_id)s )
|
|
or (
|
|
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
|
|
AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
|
|
) )
|
|
""",
|
|
{
|
|
"etudid": etudid,
|
|
"formation_id": formation_id,
|
|
"semestre_id": semestre_idx,
|
|
"date_debut": date_debut,
|
|
},
|
|
)
|
|
|
|
return cursor.dictfetchall()
|
|
|
|
|
|
def list_formsemestre_utilisateurs_uecap(formsemestre_id):
|
|
"""Liste des formsemestres pouvant utiliser une UE capitalisee de ce semestre
|
|
(et qui doivent donc etre sortis du cache si l'on modifie ce
|
|
semestre): meme code formation, meme semestre_id, date posterieure"""
|
|
cnx = ndb.GetDBConnexion()
|
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
|
cursor.execute(
|
|
"""SELECT sem.id
|
|
FROM notes_formsemestre sem, notes_formations F
|
|
WHERE sem.formation_id = F.id
|
|
and F.formation_code = %(formation_code)s
|
|
and sem.semestre_id = %(semestre_id)s
|
|
and sem.date_debut >= %(date_debut)s
|
|
and sem.id != %(formsemestre_id)s;
|
|
""",
|
|
{
|
|
"formation_code": F["formation_code"],
|
|
"semestre_id": sem["semestre_id"],
|
|
"formsemestre_id": formsemestre_id,
|
|
"date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
|
|
},
|
|
)
|
|
return [x[0] for x in cursor.fetchall()]
|