ScoDoc-Lille/app/scodoc/sco_cursus_dut.py

1021 lines
38 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2023-12-31 23:04:06 +01:00
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
2020-09-26 16:19:37 +02:00
#
# 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)
"""
2021-02-05 22:16:30 +01:00
2022-07-07 16:24:52 +02:00
from app import db
2022-02-09 23:22:00 +01:00
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
2024-07-08 23:13:45 +02:00
from app.models import (
FormSemestre,
Identite,
ScolarAutorisationInscription,
Scolog,
UniteEns,
)
2022-02-09 23:22:00 +01:00
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
2021-08-29 19:57:32 +02:00
from app import log
2024-07-08 23:13:45 +02:00
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre
from app.scodoc.codes_cursus import (
2021-02-05 22:16:30 +01:00
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
2020-09-26 16:19:37 +02:00
2021-07-09 23:31:16 +02:00
class DecisionSem(object):
2020-09-26 16:19:37 +02:00
"Decision prenable pour un semestre"
def __init__(
self,
code_etat=None,
code_etat_ues: dict = None, # { ue_id : code }
2020-09-26 16:19:37 +02:00
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 {}
2020-09-26 16:19:37 +02:00
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,
)
2020-09-26 16:19:37 +02:00
)
)
)
2022-07-07 16:24:52 +02:00
class SituationEtudCursus:
"Semestre dans un cursus"
2020-09-26 16:19:37 +02:00
2022-07-07 16:24:52 +02:00
class SituationEtudCursusClassic(SituationEtudCursus):
2020-09-26 16:19:37 +02:00
"Semestre dans un parcours"
2024-06-07 17:58:02 +02:00
def __init__(self, etud: Identite, formsemestre_id: int, nt: NotesTableCompat):
2020-09-26 16:19:37 +02:00
"""
etud: dict filled by fill_etuds_info()
2020-09-26 16:19:37 +02:00
"""
2024-06-07 17:58:02 +02:00
assert formsemestre_id == nt.formsemestre.id
2020-09-26 16:19:37 +02:00
self.etud = etud
2024-06-07 17:58:02 +02:00
self.etudid = etud.id
2020-09-26 16:19:37 +02:00
self.formsemestre_id = formsemestre_id
2024-06-07 17:58:02 +02:00
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"
2022-07-02 11:17:04 +02:00
self.nt: NotesTableCompat = nt
2022-02-09 23:22:00 +01:00
self.formation = self.nt.formsemestre.formation
2020-09-26 16:19:37 +02:00
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))
2024-06-07 17:58:02 +02:00
self.semestre_non_terminal = self.cur_sem.semestre_id != self.parcours.NB_SEM
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
2020-09-26 16:19:37 +02:00
self.semestre_non_terminal = False
# Liste des semestres du parcours de cet étudiant:
self._comp_semestres()
# Determine le semestre "precedent"
2024-06-07 17:58:02 +02:00
self._search_prev()
2020-09-26 16:19:37 +02:00
# Verifie barres
self._comp_barres()
# Verifie compensation
2024-06-07 17:58:02 +02:00
if self.prev_formsemestre and self.cur_sem.gestion_compensation:
self.can_compensate_with_prev = (
self.prev_formsemestre.id in self.can_compensate
)
2020-09-26 16:19:37 +02:00
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:
2024-06-07 17:58:02 +02:00
if (not self.cur_sem.gestion_semestrielle) and rule.conclusion[
2021-08-11 00:36:07 +02:00
3
] == "REDOSEM":
2020-09-26 16:19:37 +02:00
continue
if rule.match(state):
if rule.conclusion[0] == ADC:
# dans les regles on ne peut compenser qu'avec le PRECEDENT:
2024-06-07 17:58:02 +02:00
fiduc = self.prev_formsemestre.id
2020-09-26 16:19:37 +02:00
assert fiduc
else:
fiduc = None
# Detection d'incoherences (regles BUG)
if rule.conclusion[5] == BUG:
log(f"get_possible_choices: inconsistency: state={state}")
2020-09-26 16:19:37 +02:00
#
2021-02-05 22:16:30 +01:00
# valid_semestre = code_semestre_validant(rule.conclusion[0])
2020-09-26 16:19:37 +02:00
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 ""
2024-06-07 17:58:02 +02:00
s_idx = self.cur_sem.semestre_id # numero semestre courant
if s_idx < 0: # formation sans semestres (eg licence)
2020-09-26 16:19:37 +02:00
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'
2020-09-26 16:19:37 +02:00
if self.semestre_non_terminal and not self.all_other_validated():
passage = f"Passe en {sess_abrv}{next_s}"
2020-09-26 16:19:37 +02:00
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})"
2020-09-26 16:19:37 +02:00
elif devenir == REDOSEM:
return f"Redouble semestre (recommence en {sess_abrv}{s_idx})"
2020-09-26 16:19:37 +02:00
elif devenir == RA_OR_NEXT:
return passage + ", ou redouble année (en {sess_abrv}{s_idx - 1})"
2020-09-26 16:19:37 +02:00
elif devenir == RA_OR_RS:
return f"""Redouble semestre {sess_abrv}{s_idx}, ou redouble année (en {
sess_abrv}{s_idx - 1})"""
2020-09-26 16:19:37 +02:00
elif devenir == RS_OR_NEXT:
return f"{passage}, ou semestre {sess_abrv}{s_idx}"
2020-09-26 16:19:37 +02:00
elif devenir == NEXT_OR_NEXT2:
# coherent avec get_next_semestre_ids
return f"{passage}, ou en semestre {sess_abrv}{s_idx + 2}"
2020-09-26 16:19:37 +02:00
elif devenir == NEXT2:
return f"Passe en {sess_abrv}{s_idx + 2}"
2020-09-26 16:19:37 +02:00
else:
log(f"explique_devenir: code devenir inconnu: {devenir}")
2020-09-26 16:19:37 +02:00
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"
2024-06-07 17:58:02 +02:00
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
2020-09-26 16:19:37 +02:00
# 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(
2020-09-26 16:19:37 +02:00
range(1, self.parcours.NB_SEM + 1)
) # ensemble des indices à valider
2024-06-07 17:58:02 +02:00
if exclude_current and self.cur_sem.semestre_id in to_validate:
to_validate.remove(self.cur_sem.semestre_id)
2020-09-26 16:19:37 +02:00
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)
"""
2024-06-07 17:58:02 +02:00
s_idx = self.cur_sem.semestre_id
if not self.cur_sem.gestion_semestrielle:
2020-09-26 16:19:37 +02:00
return False # pas de semestre décalés
2024-06-07 17:58:02 +02:00
if s_idx == NO_SEMESTRE_ID or s_idx > self.parcours.NB_SEM - 2:
2020-09-26 16:19:37 +02:00
return False # n+2 en dehors du parcours
2024-06-07 17:58:02 +02:00
if self._sem_list_validated(set(range(1, s_idx))):
# antérieurs validés, teste suivant
2024-06-07 17:58:02 +02:00
n1 = s_idx + 1
for formsemestre in self.formsemestres:
2020-09-26 16:19:37 +02:00
if (
formsemestre.semestre_id == n1
and formsemestre.formation.formation_code
== self.formation.formation_code
2020-09-26 16:19:37 +02:00
):
2022-02-09 23:22:00 +01:00
nt: NotesTableCompat = res_sem.load_formsemestre_results(
formsemestre
)
2020-09-26 16:19:37 +02:00
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():
2022-02-09 23:22:00 +01:00
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)
2020-09-26 16:19:37 +02:00
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):
2024-06-07 17:58:02 +02:00
# plus ancien en tête:
self.formsemestres = self.etud.get_formsemestres(recent_first=False)
2020-09-26 16:19:37 +02:00
# Nb max d'UE et acronymes
ue_acros = {} # acronyme ue : 1
nb_max_ue = 0
2024-06-07 17:58:02 +02:00
sems = []
for formsemestre in self.formsemestres: # plus ancien en tête
2022-02-09 23:22:00 +01:00
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
2024-06-07 17:58:02 +02:00
sem = formsemestre.to_dict()
sems.append(sem)
2021-12-24 00:08:25 +01:00
ues = nt.get_ues_stat_dict(filter_sport=True)
2020-09-26 16:19:37 +02:00
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
2020-09-26 16:19:37 +02:00
# si sem peut servir à compenser le semestre courant, positionne
# can_compensate
2024-06-07 17:58:02 +02:00
if self.check_compensation_dut(sem, nt):
self.can_compensate.add(formsemestre.id)
2020-09-26 16:19:37 +02:00
2021-07-09 17:47:06 +02:00
self.ue_acros = list(ue_acros.keys())
2020-09-26 16:19:37 +02:00
self.ue_acros.sort()
self.nb_max_ue = nb_max_ue
self.sems = sems
2024-06-07 17:58:02 +02:00
def get_semestres(self) -> list[dict]:
2020-09-26 16:19:37 +02:00
"""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:
2020-09-26 16:19:37 +02:00
"""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.
2020-09-26 16:19:37 +02:00
"""
2024-06-07 17:58:02 +02:00
cur_begin_date = self.cur_sem.date_debut
cur_formation_code = self.cur_sem.formation.formation_code
2020-09-26 16:19:37 +02:00
p = []
2024-06-07 17:58:02 +02:00
for formsemestre in self.formsemestres:
inscription = formsemestre.etuds_inscriptions.get(self.etud.id)
if inscription is None:
return "non inscrit" # !!!
2024-06-07 17:58:02 +02:00
if inscription.etat == scu.DEMISSION:
2020-09-26 16:19:37 +02:00
dem = " (dem.)"
else:
dem = ""
2024-06-07 17:58:02 +02:00
if filter_futur and formsemestre.date_debut > cur_begin_date:
2020-09-26 16:19:37 +02:00
continue # skip semestres demarrant apres le courant
2024-06-07 17:58:02 +02:00
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'
2024-06-07 17:58:02 +02:00
if formsemestre.semestre_id < 0:
session_abbrv = "A" # force, cas des DUT annuels par exemple
2024-06-07 17:58:02 +02:00
p.append("%s%d%s" % (session_abbrv, -formsemestre.semestre_id, dem))
2020-09-26 16:19:37 +02:00
else:
2024-06-07 17:58:02 +02:00
p.append("%s%d%s" % (session_abbrv, formsemestre.semestre_id, dem))
2020-09-26 16:19:37 +02:00
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 = {}
2024-06-07 17:58:02 +02:00
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
2020-09-26 16:19:37 +02:00
indices = [NO_SEMESTRE_ID]
else:
2021-07-09 17:47:06 +02:00
indices = list(range(1, self.parcours.NB_SEM + 1))
2020-09-26 16:19:37 +02:00
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:
2022-02-09 23:22:00 +01:00
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
2020-09-26 16:19:37 +02:00
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)
2021-07-11 22:32:01 +02:00
self.barre_moy_ok = (isinstance(self.moy_gen, float)) and (
2021-02-05 22:16:30 +01:00
self.moy_gen >= (self.parcours.BARRE_MOY - scu.NOTES_TOLERANCE)
2020-09-26 16:19:37 +02:00
)
# conserve etat UEs
2021-12-24 00:08:25 +01:00
ue_ids = [x["ue_id"] for x in self.nt.get_ues_stat_dict(filter_sport=True)]
2020-09-26 16:19:37 +02:00
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
2024-06-07 17:58:02 +02:00
def _search_prev(self) -> FormSemestre | None:
2020-09-26 16:19:37 +02:00
"""Recherche semestre 'precedent'.
2024-06-07 17:58:02 +02:00
positionne .prev_decision
2020-09-26 16:19:37 +02:00
"""
2024-06-07 17:58:02 +02:00
self.prev_formsemestre = None
2020-09-26 16:19:37 +02:00
self.prev_decision = None
2024-06-07 17:58:02 +02:00
if len(self.formsemestres) < 2:
2020-09-26 16:19:37 +02:00
return None
# Cherche sem courant dans la liste triee par date_debut
cur = None
icur = -1
2024-06-07 17:58:02 +02:00
for cur in self.formsemestres:
2020-09-26 16:19:37 +02:00
icur += 1
2024-06-07 17:58:02 +02:00
if cur.id == self.formsemestre_id:
2020-09-26 16:19:37 +02:00
break
2024-06-07 17:58:02 +02:00
if not cur or cur.id != self.formsemestre_id:
2020-09-26 16:19:37 +02:00
log(
f"""*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={
self.formsemestre_id}, etudid={self.etudid})"""
2020-09-26 16:19:37 +02:00
)
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é
2024-06-07 17:58:02 +02:00
i = len(self.formsemestres) - 1 # par du dernier, remonte vers le passé
prev_formsemestre = None
2020-09-26 16:19:37 +02:00
while i >= 0:
if (
2024-06-07 17:58:02 +02:00
self.formsemestres[i].formation.formation_code
== self.formation.formation_code
and self.formsemestres[i].semestre_id == cur.semestre_id - 1
2020-09-26 16:19:37 +02:00
):
2024-06-07 17:58:02 +02:00
prev_formsemestre = self.formsemestres[i]
2020-09-26 16:19:37 +02:00
break
i -= 1
2024-06-07 17:58:02 +02:00
if not prev_formsemestre:
2020-09-26 16:19:37 +02:00
return None # pas de precedent trouvé
2024-06-07 17:58:02 +02:00
self.prev_formsemestre = prev_formsemestre
2020-09-26 16:19:37 +02:00
# Verifications basiques:
# ?
# Code etat du semestre precedent:
2024-06-07 17:58:02 +02:00
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_formsemestre)
2020-09-26 16:19:37 +02:00
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]
2024-06-07 17:58:02 +02:00
def get_next_semestre_ids(self, devenir: str) -> list[int]:
2020-09-26 16:19:37 +02:00
"""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]
"""
2024-06-07 17:58:02 +02:00
s_idx = self.cur_sem.semestre_id
2020-09-26 16:19:37 +02:00
if devenir == NEXT:
ids = [self._get_next_semestre_id()]
elif devenir == REDOANNEE:
2024-06-07 17:58:02 +02:00
ids = [s_idx - 1]
2020-09-26 16:19:37 +02:00
elif devenir == REDOSEM:
2024-06-07 17:58:02 +02:00
ids = [s_idx]
2020-09-26 16:19:37 +02:00
elif devenir == RA_OR_NEXT:
2024-06-07 17:58:02 +02:00
ids = [s_idx - 1, self._get_next_semestre_id()]
2020-09-26 16:19:37 +02:00
elif devenir == RA_OR_RS:
2024-06-07 17:58:02 +02:00
ids = [s_idx - 1, s_idx]
2020-09-26 16:19:37 +02:00
elif devenir == RS_OR_NEXT:
2024-06-07 17:58:02 +02:00
ids = [s_idx, self._get_next_semestre_id()]
2020-09-26 16:19:37 +02:00
elif devenir == NEXT_OR_NEXT2:
ids = [
self._get_next_semestre_id(),
2024-06-07 17:58:02 +02:00
s_idx + 2,
2020-09-26 16:19:37 +02:00
] # cohérent avec explique_devenir()
elif devenir == NEXT2:
2024-06-07 17:58:02 +02:00
ids = [s_idx + 2]
2020-09-26 16:19:37 +02:00
else:
ids = [] # reoriente ou autre: pas de next !
# clip [1..NB_SEM]
r = []
for idx in ids:
2024-06-07 17:58:02 +02:00
if 0 < idx <= self.parcours.NB_SEM:
2020-09-26 16:19:37 +02:00
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
"""
2024-06-07 17:58:02 +02:00
s_idx = self.cur_sem.semestre_id
if s_idx >= self.parcours.NB_SEM:
2020-09-26 16:19:37 +02:00
return self.parcours.NB_SEM + 1
validated = True
2024-06-07 17:58:02 +02:00
while validated and (s_idx < self.parcours.NB_SEM):
s_idx = s_idx + 1
2020-09-26 16:19:37 +02:00
# semestre s validé ?
validated = False
2024-06-07 17:58:02 +02:00
for formsemestre in self.formsemestres:
2020-09-26 16:19:37 +02:00
if (
2024-06-07 17:58:02 +02:00
formsemestre.formation.formation_code
== self.formation.formation_code
and formsemestre.semestre_id == s_idx
2020-09-26 16:19:37 +02:00
):
2022-02-09 23:22:00 +01:00
nt: NotesTableCompat = res_sem.load_formsemestre_results(
formsemestre
)
2020-09-26 16:19:37 +02:00
decision = nt.get_etud_decision_sem(self.etudid)
if decision and code_semestre_validant(decision["code"]):
validated = True
2024-06-07 17:58:02 +02:00
return s_idx
2020-09-26 16:19:37 +02:00
def valide_decision(self, decision):
2020-09-26 16:19:37 +02:00
"""Enregistre la decision (instance de DecisionSem)
Enregistre codes semestre et UE, et autorisations inscription.
"""
cnx = ndb.GetDBConnexion()
2020-09-26 16:19:37 +02:00
# -- 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
2024-06-07 17:58:02 +02:00
for formsemestre in self.formsemestres:
if (
formsemestre.id == fsid
and formsemestre.id in self.can_compensate
):
2020-09-26 16:19:37 +02:00
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,
)
2024-07-08 23:13:45 +02:00
Scolog.logdb(
2020-09-26 16:19:37 +02:00
method="validate_sem",
etudid=self.etudid,
commit=False,
2024-07-08 23:13:45 +02:00
msg=f"formsemestre_id={self.formsemestre_id} code={decision.code_etat}",
2020-09-26 16:19:37 +02:00
)
# -- decisions UEs
formsemestre_validate_ues(
self.formsemestre_id,
self.etudid,
decision.code_etat,
decision.assiduite,
)
# -- modification du code du semestre precedent
2024-06-07 17:58:02 +02:00
if self.prev_formsemestre and decision.new_code_prev:
2020-09-26 16:19:37 +02:00
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,
2024-06-07 17:58:02 +02:00
self.prev_formsemestre.id,
2020-09-26 16:19:37 +02:00
self.etudid,
decision.new_code_prev,
2021-08-11 00:36:07 +02:00
assidu=True,
2020-09-26 16:19:37 +02:00
formsemestre_id_utilise_pour_compenser=fsid,
)
2024-07-08 23:13:45 +02:00
Scolog.logdb(
2020-09-26 16:19:37 +02:00
method="validate_sem",
etudid=self.etudid,
commit=False,
2024-07-08 23:13:45 +02:00
msg=f"formsemestre_id={self.prev_formsemestre.id} code={decision.new_code_prev}",
2020-09-26 16:19:37 +02:00
)
# modifs des codes d'UE (pourraient passer de ADM a CMP, meme sans modif des notes)
formsemestre_validate_ues(
2024-06-07 17:58:02 +02:00
self.prev_formsemestre.id,
2020-09-26 16:19:37 +02:00
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...
2020-09-26 16:19:37 +02:00
)
2021-07-19 19:53:01 +02:00
sco_cache.invalidate_formsemestre(
2024-06-07 17:58:02 +02:00
formsemestre_id=self.prev_formsemestre.id
2020-09-26 16:19:37 +02:00
) # > modif decisions jury (sem, UE)
try:
2022-07-07 16:24:52 +02:00
# -- Supprime autorisations venant de ce formsemestre
autorisations = ScolarAutorisationInscription.query.filter_by(
etudid=self.etudid, origin_formsemestre_id=self.formsemestre_id
2020-09-26 16:19:37 +02:00
)
2022-07-07 16:24:52 +02:00
for autorisation in autorisations:
db.session.delete(autorisation)
db.session.flush()
# -- Enregistre autorisations inscription
2020-09-26 16:19:37 +02:00
next_semestre_ids = self.get_next_semestre_ids(decision.devenir)
for next_semestre_id in next_semestre_ids:
2022-07-07 16:24:52 +02:00
autorisation = ScolarAutorisationInscription(
etudid=self.etudid,
formation_code=self.formation.formation_code,
semestre_id=next_semestre_id,
origin_formsemestre_id=self.formsemestre_id,
2020-09-26 16:19:37 +02:00
)
2022-07-07 16:24:52 +02:00
db.session.add(autorisation)
db.session.commit()
2020-09-26 16:19:37 +02:00
except:
2022-07-07 16:24:52 +02:00
cnx.session.rollback()
2020-09-26 16:19:37 +02:00
raise
2021-07-19 19:53:01 +02:00
sco_cache.invalidate_formsemestre(
formsemestre_id=self.formsemestre_id
2020-09-26 16:19:37 +02:00
) # > modif decisions jury et autorisations inscription
if decision.formsemestre_id_utilise_pour_compenser:
# inval aussi le semestre utilisé pour compenser:
2021-07-19 19:53:01 +02:00
sco_cache.invalidate_formsemestre(
formsemestre_id=decision.formsemestre_id_utilise_pour_compenser,
2020-09-26 16:19:37 +02:00
) # > modif decision jury
for formsemestre_id in to_invalidate:
2021-07-19 19:53:01 +02:00
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
2020-09-26 16:19:37 +02:00
) # > 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
2020-09-26 16:19:37 +02:00
2022-07-07 16:24:52 +02:00
class SituationEtudCursusECTS(SituationEtudCursusClassic):
"""Gestion parcours basés sur ECTS"""
2020-09-26 16:19:37 +02:00
2024-06-07 17:58:02 +02:00
def __init__(self, etud: Identite, formsemestre_id: int, nt):
2022-07-07 16:24:52 +02:00
SituationEtudCursusClassic.__init__(self, etud, formsemestre_id, nt)
2020-09-26 16:19:37 +02:00
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)
2020-09-26 16:19:37 +02:00
Dans ce type de parcours, on n'utilise que ADM, AJ, et ADJ (?).
"""
2022-02-06 16:09:17 +01:00
etud_ects_infos = self.nt.get_etud_ects_pot(self.etudid)
2020-09-26 16:19:37 +02:00
if (
2022-02-06 16:09:17 +01:00
etud_ects_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR
and etud_ects_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR
2020-09-26 16:19:37 +02:00
):
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
# -------------------------------------------------------------------------------------------
2021-02-03 22:00:41 +01:00
_scolar_formsemestre_validation_editor = ndb.EditableTable(
2020-09-26 16:19:37 +02:00
"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",
),
2021-08-11 00:36:07 +02:00
output_formators={
"event_date": ndb.DateISOtoDMY,
},
input_formators={
"event_date": ndb.DateDMYtoISO,
"assidu": bool,
"is_external": bool,
},
2020-09-26 16:19:37 +02:00
)
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
2021-02-03 22:00:41 +01:00
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
2020-09-26 16:19:37 +02:00
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,
2021-08-11 00:36:07 +02:00
assidu=True,
2020-09-26 16:19:37 +02:00
formsemestre_id_utilise_pour_compenser=None,
):
"Update validation semestre"
args = {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"code": code,
2021-08-11 00:36:07 +02:00
"assidu": assidu,
2020-09-26 16:19:37 +02:00
}
log("formsemestre_update_validation_sem: %s" % args)
2021-02-03 22:00:41 +01:00
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
2020-09-26 16:19:37 +02:00
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
2021-09-24 00:28:09 +02:00
def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite):
2020-09-26 16:19:37 +02:00
"""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)
2022-02-09 23:22:00 +01:00
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
2021-12-24 00:08:25 +01:00
ue_ids = [x["ue_id"] for x in nt.get_ues_stat_dict(filter_sport=True)]
2020-09-26 16:19:37 +02:00
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 (
2021-07-11 22:32:01 +02:00
isinstance(ue_status["moy"], float)
2020-09-26 16:19:37 +02:00
and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE
):
code_ue = ADM
elif not isinstance(ue_status["moy"], float):
2020-09-26 16:19:37 +02:00
# 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
)
2024-07-08 23:13:45 +02:00
Scolog.logdb(
2021-09-24 00:28:09 +02:00
method="validate_ue",
etudid=etudid,
2024-07-08 23:13:45 +02:00
msg=f"ue_id={ue_id} code={code_ue}",
2021-09-24 00:28:09 +02:00
commit=False,
)
2024-07-08 23:13:45 +02:00
db.session.commit()
2020-09-26 16:19:37 +02:00
cnx.commit()
def do_formsemestre_validate_ue(
cnx,
nt,
formsemestre_id,
etudid,
ue_id,
code,
moy_ue=None,
date=None,
semestre_id=None,
2021-08-10 12:57:38 +02:00
is_external=False,
2020-09-26 16:19:37 +02:00
):
"""Ajoute ou change validation UE"""
2022-02-06 16:09:17 +01:00
if semestre_id is None:
ue = UniteEns.query.get_or_404(ue_id)
semestre_id = ue.semestre_idx
2020-09-26 16:19:37 +02:00
args = {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"ue_id": ue_id,
"semestre_id": semestre_id,
"is_external": is_external,
"moy_ue": moy_ue,
2020-09-26 16:19:37 +02:00
}
if date:
args["event_date"] = date
# delete existing
2021-02-03 22:00:41 +01:00
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
2020-09-26 16:19:37 +02:00
try:
cond = "etudid = %(etudid)s and ue_id=%(ue_id)s"
if formsemestre_id:
cond += " and formsemestre_id=%(formsemestre_id)s"
if semestre_id:
2022-02-06 16:09:17 +01:00
cond += " and (semestre_id=%(semestre_id)s or semestre_id is NULL)"
log(f"formsemestre_validate_ue: deleting where {cond}, args={args})")
2020-09-26 16:19:37 +02:00
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)
2023-07-11 22:06:24 +02:00
args["moy_ue"] = ue_status["moy"] if ue_status else ""
2022-02-06 16:09:17 +01:00
log("formsemestre_validate_ue: create %s" % args)
if code is not None:
2020-09-26 16:19:37 +02:00
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):
2020-09-26 16:19:37 +02:00
"""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
"""
2021-06-15 13:59:56 +02:00
cnx = ndb.GetDBConnexion()
2020-09-26 16:19:37 +02:00
validations = scolar_formsemestre_validation_list(
cnx, args={"formsemestre_id": formsemestre_id, "is_external": False}
2020-09-26 16:19:37 +02:00
)
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 !
"""
2021-02-03 22:00:41 +01:00
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
2020-09-26 16:19:37 +02:00
cursor.execute(
2022-07-07 16:24:52 +02:00
"""SELECT mi.*
2021-08-10 12:57:38 +02:00
FROM notes_moduleimpl mi, notes_modules mo, notes_ue ue, notes_moduleimpl_inscription i
2022-07-07 16:24:52 +02:00
WHERE i.etudid = %(etudid)s
2021-08-10 12:57:38 +02:00
and i.moduleimpl_id=mi.id
2020-09-26 16:19:37 +02:00
and mi.formsemestre_id = %(formsemestre_id)s
2021-08-10 12:57:38 +02:00
and mi.module_id = mo.id
2020-09-26 16:19:37 +02:00
and mo.ue_id = %(ue_id)s
""",
{"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
)
return len(cursor.fetchall())