WIP: modernisation code jurys

This commit is contained in:
Emmanuel Viennet 2024-06-07 17:58:02 +02:00
parent e0208d0650
commit 320cfbebc8
15 changed files with 251 additions and 220 deletions

View File

@ -44,7 +44,7 @@ from app.scodoc import sco_cursus_dut
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
def __init__(self, etud: Identite, formsemestre_id: int, res: ResultatsSemestreBUT):
super().__init__(etud, formsemestre_id, res)
# Ajustements pour le BUT
self.can_compensate_with_prev = False # jamais de compensation à la mode DUT

View File

@ -359,14 +359,15 @@ class Identite(models.ScoDocModel):
"Le mail associé à la première adresse de l'étudiant, ou None"
return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None
def get_formsemestres(self) -> list:
def get_formsemestres(self, recent_first=True) -> list:
"""Liste des formsemestres dans lesquels l'étudiant est (a été) inscrit,
triée par date_debut
triée par date_debut, le plus récent d'abord (comme "sems" de scodoc7)
(si recent_first=False, le plus ancien en tête)
"""
return sorted(
[ins.formsemestre for ins in self.formsemestre_inscriptions],
key=attrgetter("date_debut"),
reverse=True,
reverse=recent_first,
)
def get_modimpls_by_formsemestre(

View File

@ -46,6 +46,8 @@ class UniteEns(models.ScoDocModel):
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
coefficient = db.Column(db.Float)
# id de l'élément Apogée du RCUE (utilisé pour les UEs de sem. pair du BUT)
code_apogee_rcue = db.Column(db.String(APO_CODE_STR_LEN))
# coef. pour le calcul de moyennes de RCUE. Par défaut, 1.
coef_rcue = db.Column(db.Float, nullable=False, default=1.0, server_default="1.0")

View File

@ -48,9 +48,9 @@ import numpy as np
from app import log
from app.but import jury_but
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.comp.res_but import ResultatsSemestreBUT
from app.models import (
ApcValidationAnnee,
ApcValidationRCUE,
@ -132,7 +132,7 @@ class ApoEtud(dict):
"Vrai si BUT"
self.col_elts = {}
"{'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}"
self.etud: Identite = None
self.etud: Identite | None = None
"etudiant ScoDoc associé"
self.etat = None # ETUD_OK, ...
self.is_nar = False
@ -171,24 +171,18 @@ class ApoEtud(dict):
Sinon, cherche le semestre, et met l'état à ETUD_OK ou ETUD_NON_INSCRIT.
"""
# futur: #WIP
# etud: Identite = Identite.query.filter_by(code_nip=self["nip"], dept_id=g.scodoc_dept_id).first()
# self.etud = etud
etuds = sco_etud.get_etud_info(code_nip=self["nip"], filled=True)
if not etuds:
self.etud = Identite.query.filter_by(
code_nip=self["nip"], dept_id=g.scodoc_dept_id
).first()
if not self.etud:
# pas dans ScoDoc
self.etud = None
self.log.append("non inscrit dans ScoDoc")
self.etat = ETUD_ORPHELIN
else:
# futur: #WIP
# formsemestre_ids = {
# ins.formsemestre_id for ins in etud.formsemestre_inscriptions
# }
# in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
self.etud = etuds[0]
# cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape:
formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]}
formsemestre_ids = {
ins.formsemestre_id for ins in self.etud.formsemestre_inscriptions
}
in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
if not in_formsemestre_ids:
self.log.append(
@ -267,13 +261,17 @@ class ApoEtud(dict):
Args:
code (str): code apo de l'element cherché
sem (dict): semestre dans lequel on cherche l'élément
Utilise notamment:
cur_sem (dict): semestre "courant" pour résultats annuels (VET)
autre_sem (dict): autre semestre utilisé pour calculer les résultats annuels (VET)
Returns:
dict: with N, B, J, R keys, ou None si elt non trouvé
"""
etudid = self.etud["etudid"]
if not self.etud:
return None
etudid = self.etud.id
if not self.cur_res:
log("search_elt_in_sem: no cur_res !")
return None
@ -377,6 +375,8 @@ class ApoEtud(dict):
if module_code_found:
return VOID_APO_RES
# RCUE du BUT
deca = jury_but.DecisionsProposeesAnnee(self.etud, formsemestre)
#
return None # element Apogee non trouvé dans ce semestre

View File

@ -751,7 +751,7 @@ def formsemestre_get_assiduites_count(
) -> tuple[int, int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
Utilise un cache.
Utilise un cache (si moduleimpl_id n'est pas spécifié).
"""
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
return get_assiduites_count_in_interval(
@ -779,7 +779,7 @@ def get_assiduites_count_in_interval(
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
On peut spécifier les dates comme datetime ou iso.
Utilise un cache.
Utilise un cache (si moduleimpl_id n'est pas spécifié).
"""
date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d")
date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d")

View File

@ -34,13 +34,13 @@ from app.scodoc import sco_cursus_dut
from app.comp.res_compat import NotesTableCompat
from app.comp import res_sem
from app.models import FormSemestre
from app.models import FormSemestre, Identite
import app.scodoc.notesdb as ndb
# SituationEtudParcours -> get_situation_etud_cursus
def get_situation_etud_cursus(
etud: dict, formsemestre_id: int
etud: Identite, formsemestre_id: int
) -> sco_cursus_dut.SituationEtudCursus:
"""renvoie une instance de SituationEtudCursus (ou sous-classe spécialisée)"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)

View File

@ -31,7 +31,7 @@
from app import db
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription
from app.models import FormSemestre, Identite, ScolarAutorisationInscription, UniteEns
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@ -115,14 +115,22 @@ class SituationEtudCursus:
class SituationEtudCursusClassic(SituationEtudCursus):
"Semestre dans un parcours"
def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat):
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["etudid"]
self.etudid = etud.id
self.formsemestre_id = formsemestre_id
self.sem = sco_formsemestre.get_formsemestre(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
@ -130,18 +138,20 @@ class SituationEtudCursusClassic(SituationEtudCursus):
# 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 = 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.prev_formsemestre_id = self._search_prev()
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"]
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
@ -170,14 +180,14 @@ class SituationEtudCursusClassic(SituationEtudCursus):
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[
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
fiduc = self.prev_formsemestre.id
assert fiduc
else:
fiduc = None
@ -203,8 +213,8 @@ class SituationEtudCursusClassic(SituationEtudCursus):
"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)
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()
@ -219,27 +229,27 @@ class SituationEtudCursusClassic(SituationEtudCursus):
elif devenir == REO:
return "Réorienté"
elif devenir == REDOANNEE:
return "Redouble année (recommence %s%s)" % (SA, (s - 1))
return "Redouble année (recommence %s%s)" % (SA, (s_idx - 1))
elif devenir == REDOSEM:
return "Redouble semestre (recommence en %s%s)" % (SA, s)
return "Redouble semestre (recommence en %s%s)" % (SA, s_idx)
elif devenir == RA_OR_NEXT:
return passage + ", ou redouble année (en %s%s)" % (SA, (s - 1))
return passage + ", ou redouble année (en %s%s)" % (SA, (s_idx - 1))
elif devenir == RA_OR_RS:
return "Redouble semestre %s%s, ou redouble année (en %s%s)" % (
SA,
s,
s_idx,
SA,
s - 1,
s_idx - 1,
)
elif devenir == RS_OR_NEXT:
return passage + ", ou semestre %s%s" % (SA, s)
return passage + ", ou semestre %s%s" % (SA, s_idx)
elif devenir == NEXT_OR_NEXT2:
return passage + ", ou en semestre %s%s" % (
SA,
s + 2,
s_idx + 2,
) # coherent avec get_next_semestre_ids
elif devenir == NEXT2:
return "Passe en %s%s" % (SA, s + 2)
return "Passe en %s%s" % (SA, s_idx + 2)
else:
log("explique_devenir: code devenir inconnu: %s" % devenir)
return "Code devenir inconnu !"
@ -258,7 +268,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
def _sems_validated(self, exclude_current=False):
"True si semestres du parcours validés"
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
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"])
@ -266,8 +276,8 @@ class SituationEtudCursusClassic(SituationEtudCursus):
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"])
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):
@ -275,14 +285,14 @@ class SituationEtudCursusClassic(SituationEtudCursus):
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"]:
s_idx = self.cur_sem.semestre_id
if not self.cur_sem.gestion_semestrielle:
return False # pas de semestre décalés
if n == NO_SEMESTRE_ID or n > self.parcours.NB_SEM - 2:
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, n))):
if self._sem_list_validated(set(range(1, s_idx))):
# antérieurs validé, teste suivant
n1 = n + 1
n1 = s_idx + 1
for sem in self.get_semestres():
if (
sem["semestre_id"] == n1
@ -315,19 +325,17 @@ class SituationEtudCursusClassic(SituationEtudCursus):
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()
# 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
for sem in sems:
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
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
@ -338,14 +346,15 @@ class SituationEtudCursusClassic(SituationEtudCursus):
sem["formation_code"] = formsemestre.formation.formation_code
# si sem peut servir à compenser le semestre courant, positionne
# can_compensate
sem["can_compensate"] = self.check_compensation_dut(sem, nt)
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):
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
@ -355,24 +364,30 @@ class SituationEtudCursusClassic(SituationEtudCursus):
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.sem["dateord"]
cur_formation_code = self.sem["formation_code"]
cur_begin_date = self.cur_sem.date_debut
cur_formation_code = self.cur_sem.formation.formation_code
p = []
for s in self.sems:
if s["ins"]["etat"] == scu.DEMISSION:
for formsemestre in self.formsemestres:
inscription = formsemestre.etuds_inscriptions.get(self.etud.id)
if inscription is None:
raise ValueError("Etudiant non inscrit au semestre") # bug
if inscription.etat == scu.DEMISSION:
dem = " (dem.)"
else:
dem = ""
if filter_futur and s["dateord"] > cur_begin_date:
if filter_futur and formsemestre.date_debut > cur_begin_date:
continue # skip semestres demarrant apres le courant
if filter_formation_code and s["formation_code"] != cur_formation_code:
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 s["semestre_id"] < 0:
if formsemestre.semestre_id < 0:
session_abbrv = "A" # force, cas des DUT annuels par exemple
p.append("%s%d%s" % (session_abbrv, -s["semestre_id"], dem))
p.append("%s%d%s" % (session_abbrv, -formsemestre.semestre_id, dem))
else:
p.append("%s%d%s" % (session_abbrv, s["semestre_id"], dem))
p.append("%s%d%s" % (session_abbrv, formsemestre.semestre_id, dem))
return ", ".join(p)
def get_parcours_decisions(self):
@ -381,7 +396,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
Returns: { semestre_id : code }
"""
r = {}
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
indices = [NO_SEMESTRE_ID]
else:
indices = list(range(1, self.parcours.NB_SEM + 1))
@ -424,22 +439,22 @@ class SituationEtudCursusClassic(SituationEtudCursus):
"true si ce semestre pourrait etre compensé par un autre (e.g. barres UE > 8)"
return self.barres_ue_ok
def _search_prev(self):
def _search_prev(self) -> FormSemestre | None:
"""Recherche semestre 'precedent'.
return prev_formsemestre_id
positionne .prev_decision
"""
self.prev = None
self.prev_formsemestre = None
self.prev_decision = None
if len(self.sems) < 2:
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.sems:
for cur in self.formsemestres:
icur += 1
if cur["formsemestre_id"] == self.formsemestre_id:
if cur.id == self.formsemestre_id:
break
if not cur or cur["formsemestre_id"] != self.formsemestre_id:
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})"
)
@ -447,60 +462,59 @@ class SituationEtudCursusClassic(SituationEtudCursus):
# 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
i = len(self.formsemestres) - 1 # par du dernier, remonte vers le passé
prev_formsemestre = None
while i >= 0:
if (
self.sems[i]["formation_code"] == self.formation.formation_code
and self.sems[i]["semestre_id"] == cur["semestre_id"] - 1
self.formsemestres[i].formation.formation_code
== self.formation.formation_code
and self.formsemestres[i].semestre_id == cur.semestre_id - 1
):
prev = self.sems[i]
prev_formsemestre = self.formsemestres[i]
break
i -= 1
if not prev:
if not prev_formsemestre:
return None # pas de precedent trouvé
self.prev = prev
self.prev_formsemestre = prev_formsemestre
# Verifications basiques:
# ?
# Code etat du semestre precedent:
formsemestre = FormSemestre.query.get_or_404(prev["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
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]
return self.prev["formsemestre_id"]
def get_next_semestre_ids(self, devenir):
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 = self.sem["semestre_id"]
s_idx = self.cur_sem.semestre_id
if devenir == NEXT:
ids = [self._get_next_semestre_id()]
elif devenir == REDOANNEE:
ids = [s - 1]
ids = [s_idx - 1]
elif devenir == REDOSEM:
ids = [s]
ids = [s_idx]
elif devenir == RA_OR_NEXT:
ids = [s - 1, self._get_next_semestre_id()]
ids = [s_idx - 1, self._get_next_semestre_id()]
elif devenir == RA_OR_RS:
ids = [s - 1, s]
ids = [s_idx - 1, s_idx]
elif devenir == RS_OR_NEXT:
ids = [s, self._get_next_semestre_id()]
ids = [s_idx, self._get_next_semestre_id()]
elif devenir == NEXT_OR_NEXT2:
ids = [
self._get_next_semestre_id(),
s + 2,
s_idx + 2,
] # cohérent avec explique_devenir()
elif devenir == NEXT2:
ids = [s + 2]
ids = [s_idx + 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:
if 0 < idx <= self.parcours.NB_SEM:
r.append(idx)
return r
@ -508,27 +522,27 @@ class SituationEtudCursusClassic(SituationEtudCursus):
"""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:
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 < self.parcours.NB_SEM):
s = s + 1
while validated and (s_idx < self.parcours.NB_SEM):
s_idx = s_idx + 1
# semestre s validé ?
validated = False
for sem in self.sems:
for formsemestre in self.formsemestres:
if (
sem["formation_code"] == self.formation.formation_code
and sem["semestre_id"] == s
formsemestre.formation.formation_code
== self.formation.formation_code
and formsemestre.semestre_id == s_idx
):
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
return s_idx
def valide_decision(self, decision):
"""Enregistre la decision (instance de DecisionSem)
@ -543,8 +557,11 @@ class SituationEtudCursusClassic(SituationEtudCursus):
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"]:
for formsemestre in self.formsemestres:
if (
formsemestre.id == fsid
and formsemestre.id in self.can_compensate
):
ok = True
break
if not ok:
@ -585,7 +602,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
decision.assiduite,
)
# -- modification du code du semestre precedent
if self.prev and decision.new_code_prev:
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
@ -593,7 +610,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
fsid = None
to_invalidate += formsemestre_update_validation_sem(
cnx,
self.prev["formsemestre_id"],
self.prev_formsemestre.id,
self.etudid,
decision.new_code_prev,
assidu=True,
@ -605,18 +622,18 @@ class SituationEtudCursusClassic(SituationEtudCursus):
etudid=self.etudid,
commit=False,
msg="formsemestre_id=%s code=%s"
% (self.prev["formsemestre_id"], decision.new_code_prev),
% (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.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"]
formsemestre_id=self.prev_formsemestre.id
) # > modif decisions jury (sem, UE)
try:
@ -698,7 +715,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
class SituationEtudCursusECTS(SituationEtudCursusClassic):
"""Gestion parcours basés sur ECTS"""
def __init__(self, etud, formsemestre_id, nt):
def __init__(self, etud: Identite, formsemestre_id: int, nt):
SituationEtudCursusClassic.__init__(self, etud, formsemestre_id, nt)
def could_be_compensated(self):

View File

@ -116,7 +116,7 @@ def formsemestre_validation_etud_form(
check = True
etud_d = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
Se = sco_cursus.get_situation_etud_cursus(etud_d, formsemestre_id)
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
if not Se.sem["etat"]:
raise ScoValueError("validation: semestre verrouille")
@ -262,8 +262,8 @@ def formsemestre_validation_etud_form(
return "\n".join(H + footer)
# Infos si pas de semestre précédent
if not Se.prev:
if Se.sem["semestre_id"] == 1:
if not Se.prev_formsemestre:
if Se.cur_sem.semestre_id == 1:
H.append("<p>Premier semestre (pas de précédent)</p>")
else:
H.append("<p>Pas de semestre précédent !</p>")
@ -274,7 +274,7 @@ def formsemestre_validation_etud_form(
f"""Le jury n'a pas statué sur le semestre précédent ! (<a href="{
url_for("notes.formsemestre_validation_etud_form",
scodoc_dept=g.scodoc_dept,
formsemestre_id=Se.prev["formsemestre_id"],
formsemestre_id=Se.prev_formsemestre.id,
etudid=etudid)
}">le faire maintenant</a>)
"""
@ -310,9 +310,9 @@ def formsemestre_validation_etud_form(
H.append("</p>")
# Cas particulier pour ATJ: corriger precedent avant de continuer
if Se.prev_decision and Se.prev_decision["code"] == ATJ:
if Se.prev_formsemestre and Se.prev_decision and Se.prev_decision["code"] == ATJ:
H.append(
"""<div class="sfv_warning"><p>La décision du semestre précédent est en
f"""<div class="sfv_warning"><p>La décision du semestre précédent est en
<b>attente</b> à cause d\'un <b>problème d\'assiduité<b>.</p>
<p>Vous devez la corriger avant de continuer ce jury. Soit vous considérez que le
problème d'assiduité n'est pas réglé et choisissez de ne pas valider le semestre
@ -320,14 +320,16 @@ def formsemestre_validation_etud_form(
l'assiduité.</p>
<form method="get" action="formsemestre_validation_etud_form">
<input type="submit" value="Statuer sur le semestre précédent"/>
<input type="hidden" name="formsemestre_id" value="%s"/>
<input type="hidden" name="etudid" value="%s"/>
<input type="hidden" name="desturl" value="formsemestre_validation_etud_form?etudid=%s&formsemestre_id=%s"/>
<input type="hidden" name="formsemestre_id" value="{Se.prev_formsemestre.id}"/>
<input type="hidden" name="etudid" value="{etudid}"/>
<input type="hidden" name="desturl" value="{
url_for("notes.formsemestre_validation_etud_form",
etudid=etudid, formsemestre_id=formsemestre_id, scodoc_dept=g.scodoc_dept
)}"/>
"""
% (Se.prev["formsemestre_id"], etudid, etudid, formsemestre_id)
)
if sortcol:
H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)
H.append(f"""<input type="hidden" name="sortcol" value="{sortcol}"/>""")
H.append("</form></div>")
H.append(html_sco_header.sco_footer())
@ -405,7 +407,7 @@ def formsemestre_validation_etud(
sortcol=None,
):
"""Enregistre validation"""
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
etud = Identite.get_etud(etudid)
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
# retrouve la decision correspondant au code:
choices = Se.get_possible_choices(assiduite=True)
@ -438,7 +440,7 @@ def formsemestre_validation_etud_manu(
"""Enregistre validation"""
if assidu:
assidu = True
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
etud = Identite.get_etud(etudid)
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
if code_etat in Se.parcours.UNUSED_CODES:
raise ScoValueError("code decision invalide dans ce parcours")
@ -494,32 +496,35 @@ def decisions_possible_rows(Se, assiduite, subtitle="", trclass=""):
choices = Se.get_possible_choices(assiduite=assiduite)
if not choices:
return ""
TitlePrev = ""
if Se.prev:
if Se.prev["semestre_id"] >= 0:
TitlePrev = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.prev["semestre_id"])
prev_title = ""
if Se.prev_formsemestre:
if Se.prev_formsemestre.semestre_id >= 0:
prev_title = "%s%d" % (
Se.parcours.SESSION_ABBRV,
Se.prev_formsemestre.semestre_id,
)
else:
TitlePrev = "Prec."
prev_title = "Prec."
if Se.sem["semestre_id"] >= 0:
TitleCur = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.sem["semestre_id"])
if Se.cur_sem.semestre_id >= 0:
cur_title = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.cur_sem.semestre_id)
else:
TitleCur = Se.parcours.SESSION_NAME
cur_title = Se.parcours.SESSION_NAME
H = [
'<tr class="%s titles"><th class="sfv_subtitle">%s</em></th>'
% (trclass, subtitle)
]
if Se.prev:
H.append("<th>Code %s</th>" % TitlePrev)
H.append("<th>Code %s</th><th>Devenir</th></tr>" % TitleCur)
if Se.prev_formsemestre:
H.append(f"<th>Code {prev_title}</th>")
H.append(f"<th>Code {cur_title}</th><th>Devenir</th></tr>")
for ch in choices:
H.append(
"""<tr class="%s"><td title="règle %s"><input type="radio" name="codechoice" value="%s" onClick="document.getElementById('subut').disabled=false;">"""
% (trclass, ch.rule_id, ch.codechoice)
)
H.append("%s </input></td>" % ch.explication)
if Se.prev:
if Se.prev_formsemestre:
H.append('<td class="centercell">%s</td>' % _dispcode(ch.new_code_prev))
H.append(
'<td class="centercell">%s</td><td>%s</td>'
@ -535,7 +540,6 @@ def formsemestre_recap_parcours_table(
etudid,
with_links=False,
with_all_columns=True,
a_url="",
sem_info=None,
show_details=False,
):
@ -576,14 +580,14 @@ def formsemestre_recap_parcours_table(
H.append("<th></th></tr>")
num_sem = 0
for sem in situation_etud_cursus.get_semestres():
is_prev = situation_etud_cursus.prev and (
situation_etud_cursus.prev["formsemestre_id"] == sem["formsemestre_id"]
for formsemestre in situation_etud_cursus.formsemestres:
is_prev = situation_etud_cursus.prev_formsemestre and (
situation_etud_cursus.prev_formsemestre.id == formsemestre.id
)
is_cur = situation_etud_cursus.formsemestre_id == sem["formsemestre_id"]
is_cur = situation_etud_cursus.formsemestre_id == formsemestre.id
num_sem += 1
dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid])
dpv = sco_pv_dict.dict_pvjury(formsemestre.id, etudids=[etudid])
pv = dpv["decisions"][0]
decision_sem = pv["decision_sem"]
decisions_ue = pv["decisions_ue"]
@ -592,7 +596,6 @@ def formsemestre_recap_parcours_table(
else:
ass = ""
formsemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if is_cur:
type_sem = "*" # now unused
@ -603,20 +606,24 @@ def formsemestre_recap_parcours_table(
else:
type_sem = ""
class_sem = "sem_autre"
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
if (
formsemestre.formation.formation_code
!= situation_etud_cursus.formation.formation_code
):
class_sem += " sem_autre_formation"
if sem["bul_bgcolor"]:
bgcolor = sem["bul_bgcolor"]
else:
bgcolor = "background-color: rgb(255,255,240)"
bgcolor = (
formsemestre.bul_bgcolor
if formsemestre.bul_bgcolor
else "background-color: rgb(255,255,240)"
)
# 1ere ligne: titre sem, decision, acronymes UE
H.append('<tr class="%s rcp_l1 sem_%s">' % (class_sem, sem["formsemestre_id"]))
H.append('<tr class="%s rcp_l1 sem_%s">' % (class_sem, formsemestre.id))
if is_cur:
pm = ""
elif is_prev:
pm = minuslink % sem["formsemestre_id"]
pm = minuslink % formsemestre.id
else:
pm = plusminus % sem["formsemestre_id"]
pm = plusminus % formsemestre.id
inscr = formsemestre.etuds_inscriptions.get(etudid)
parcours_name = ""
@ -638,9 +645,12 @@ def formsemestre_recap_parcours_table(
H.append(
f"""
<td class="rcp_type_sem" style="background-color:{bgcolor};">{num_sem}{pm}</td>
<td class="datedebut">{sem['mois_debut']}</td>
<td class="datedebut">{formsemestre.mois_debut()}</td>
<td class="rcp_titre_sem"><a class="formsemestre_status_link"
href="{a_url}formsemestre_bulletinetud?formsemestre_id={formsemestre.id}&etudid={etudid}"
href="{
url_for("notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id, etudid=etudid
)}"
title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a>
"""
)
@ -675,7 +685,7 @@ def formsemestre_recap_parcours_table(
ues = [
ue
for ue in ues
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue.id)
if etud_est_inscrit_ue(cnx, etudid, formsemestre.id, ue.id)
or etud_ue_status[ue.id]["is_capitalized"]
]
@ -697,7 +707,7 @@ def formsemestre_recap_parcours_table(
H.append("<td></td>")
H.append("</tr>")
# 2eme ligne: notes
H.append(f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">""")
H.append(f"""<tr class="{class_sem} rcp_l2 sem_{formsemestre.id}">""")
H.append(
f"""<td class="rcp_type_sem"
style="background-color:{bgcolor};">&nbsp;</td>"""
@ -706,21 +716,28 @@ def formsemestre_recap_parcours_table(
default_sem_info = '<span class="fontred">[sem. précédent]</span>'
else:
default_sem_info = ""
if not sem["etat"]: # locked
if not formsemestre.etat: # locked
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
default_sem_info += lockicon
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
default_sem_info += f"""Autre formation: {sem["formation_code"]}"""
if (
formsemestre.formation.formation_code
!= situation_etud_cursus.formation.formation_code
):
default_sem_info += (
f"""Autre formation: {formsemestre.formation.formation_code}"""
)
H.append(
'<td class="datefin">%s</td><td class="sem_info">%s</td>'
% (sem["mois_fin"], sem_info.get(sem["formsemestre_id"], default_sem_info))
% (formsemestre.mois_fin(), sem_info.get(formsemestre.id, default_sem_info))
)
# Moy Gen (sous le code decision)
H.append(
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
)
# Absences (nb d'abs non just. dans ce semestre)
nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0]
nbabsnj = sco_assiduites.formsemestre_get_assiduites_count(
etudid, formsemestre
)[0]
H.append(f"""<td class="rcp_abs">{nbabsnj}</td>""")
# UEs
@ -767,26 +784,30 @@ def formsemestre_recap_parcours_table(
H.append("<td></td>")
if with_links:
H.append(
'<td><a href="%sformsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s">modifier</a></td>'
% (a_url, sem["formsemestre_id"], etudid)
f"""<td><a class="stdlink" href="{
url_for("notes.formsemestre_validation_etud_form", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id, etudid=etudid
)}">modifier</a></td>"""
)
H.append("</tr>")
# 3eme ligne: ECTS
if (
sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"])
sco_preferences.get_preference("bul_show_ects", formsemestre.id)
or nt.parcours.ECTS_ONLY
):
etud_ects_infos = nt.get_etud_ects_pot(etudid) # ECTS potentiels
H.append(
f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">
f"""<tr class="{class_sem} rcp_l2 sem_{formsemestre.id}">
<td class="rcp_type_sem" style="background-color:{bgcolor};">&nbsp;</td>
<td></td>"""
)
# Total ECTS (affiché sous la moyenne générale)
H.append(
f"""<td class="sem_ects_tit"><a title="crédit acquis">ECTS:</a></td>
<td class="sem_ects">{pv.get("sum_ects",0):2.2g} / {etud_ects_infos["ects_total"]:2.2g}</td>
<td class="sem_ects">{
pv.get("sum_ects",0):2.2g} / {etud_ects_infos["ects_total"]:2.2g
}</td>
<td class="rcp_abs"></td>
"""
)
@ -865,7 +886,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
# précédent n'est pas géré dans ScoDoc (code ADC_)
# log(str(Se.sems))
for sem in Se.sems:
if sem["can_compensate"]:
if sem["formsemestre_id"] in Se.can_compensate:
H.append(
'<option value="%s_%s">Admis par compensation avec S%s (%s)</option>'
% (
@ -882,7 +903,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
H.append("</select></td></tr>")
# Choix code semestre precedent:
if Se.prev:
if Se.prev_formsemestre:
H.append(
'<tr><td>Code semestre précédent: </td><td><select name="new_code_prev"><option value="">Choisir une décision...</option>'
)
@ -975,7 +996,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
conflicts = [] # liste des etudiants avec decision differente déjà saisie
with sco_cache.DeferredSemCacheManager():
for etudid in etudids:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
etud = Identite.get_etud(etudid)
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
{"etudid": etudid, "formsemestre_id": formsemestre_id}
@ -984,7 +1005,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
# Conditions pour validation automatique:
if ins["etat"] == scu.INSCRIT and (
(
(not Se.prev)
(not Se.prev_formsemestre)
or (
Se.prev_decision and Se.prev_decision["code"] in (ADM, ADC, ADJ)
)
@ -1055,8 +1076,8 @@ def do_formsemestre_validation_auto(formsemestre_id):
f"""<li><a href="{
url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id,
etudid=etud["etudid"], check=1)
}">{etud["nomprenom"]}</li>"""
etudid=etud.id, check=1)
}">{etud_d["nomprenom"]}</li>"""
)
H.append("</ul>")
H.append(

View File

@ -466,9 +466,9 @@ def etud_add_group_infos(
etud['groupes'] = "TDB, Gr2, TPB1"
etud['partitionsgroupes'] = "Groupes TD:TDB, Groupes TP:Gr2 (...)"
"""
etud[
"partitions"
] = collections.OrderedDict() # partition_id : group + partition_name
etud["partitions"] = (
collections.OrderedDict()
) # partition_id : group + partition_name
if not formsemestre_id:
etud["groupes"] = ""
return etud
@ -1409,20 +1409,16 @@ def groups_auto_repartition(partition: Partition):
return flask.redirect(dest_url)
def _get_prev_moy(etudid, formsemestre_id):
def _get_prev_moy(etudid: int, formsemestre_id: int) -> float | str:
"""Donne la derniere moyenne generale calculee pour cette étudiant,
ou 0 si on n'en trouve pas (nouvel inscrit,...).
"""
info = sco_etud.get_etud_info(etudid=etudid, filled=True)
if not info:
raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
etud = info[0]
etud = Identite.get_etud(etudid)
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
if Se.prev:
prev_sem = db.session.get(FormSemestre, Se.prev["formsemestre_id"])
if Se.prev_formsemestre:
prev_sem = db.session.get(FormSemestre, Se.prev_formsemestre.id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)
return nt.get_etud_moy_gen(etudid)
else:
return nt.get_etud_moy_gen(etud.id)
return 0.0

View File

@ -27,10 +27,8 @@
"""Exports groupes
"""
from flask import request
from app.scodoc import notesdb as ndb
from app.scodoc import sco_excel
from app.scodoc import sco_groups_view
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
@ -83,9 +81,7 @@ def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
"date_str": "Date",
"comment": "Annotation",
},
origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
page_title=f"Annotations sur les étudiants de {groups_infos.groups_titles}",
caption="Annotations",
base_url=groups_infos.base_url,

View File

@ -39,7 +39,7 @@ from flask import url_for, g, request
from flask_login import current_user
from app import db
from app.models import FormSemestre
from app.models import FormSemestre, Identite
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_excel
@ -585,8 +585,8 @@ def groups_table(
etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
etud_info["_prenom_target"] = fiche_url
etud_info["_nom_disp_td_attrs"] = (
'id="%s" class="etudinfo"' % (etud_info["etudid"])
etud_info["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (
etud_info["etudid"]
)
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
if etud_info["etat"] == "D":
@ -862,21 +862,25 @@ def groups_table(
# et ajoute infos inscription
for m in groups_infos.members:
etud_info = sco_etud.get_etud_info(m["etudid"], filled=True)[0]
# TODO utiliser Identite
etud = Identite.get_etud(m["etudid"])
m.update(etud_info)
sco_etud.etud_add_lycee_infos(etud_info)
# et ajoute le parcours
Se = sco_cursus.get_situation_etud_cursus(
etud_info, groups_infos.formsemestre_id
etud, groups_infos.formsemestre_id
)
m["parcours"] = Se.get_cursus_descr()
m["code_cursus"], _ = sco_report.get_code_cursus_etud(
etud_info["etudid"], sems=etud_info["sems"]
etud.id, formsemestres=etud.get_formsemestres()
)
# TODO utiliser Identite:
rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
title = "etudiants_%s" % groups_infos.groups_filename
title = f"etudiants_{groups_infos.groups_filename}"
xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title)
filename = title
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
return scu.send_file(
xls, filename=title, suffix=scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE
)
else:
raise ScoValueError("unsupported format")

View File

@ -264,14 +264,13 @@ def fiche_etud(etudid=None):
sem_info[formsemestre.id] = grlink
if inscriptions:
Se = sco_cursus.get_situation_etud_cursus(info, info["last_formsemestre_id"])
Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"])
info["liste_inscriptions"] = formsemestre_recap_parcours_table(
Se,
etudid,
with_links=False,
sem_info=sem_info,
with_all_columns=False,
a_url="Notes/",
)
info["link_bul_pdf"] = (
"""<span class="link_bul_pdf fontred">PDF interdits par l'admin.</span>"""

View File

@ -81,14 +81,11 @@ def feuille_preparation_jury(formsemestre_id):
nbabs = {}
nbabsjust = {}
for etud in etuds:
Se = sco_cursus.get_situation_etud_cursus(
etud.to_dict_scodoc7(), formsemestre_id
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
if Se.prev_formsemestre:
ntp: NotesTableCompat = res_sem.load_formsemestre_results(
Se.prev_formsemestre
)
if Se.prev:
formsemestre_prev = FormSemestre.query.get_or_404(
Se.prev["formsemestre_id"]
)
ntp: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre_prev)
for ue in ntp.get_ues_stat_dict(filter_sport=True):
ue_status = ntp.get_etud_ue_status(etud.id, ue["ue_id"])
ue_code_s = (
@ -110,7 +107,7 @@ def feuille_preparation_jury(formsemestre_id):
moy_ue[ue_code_s][etud.id] = ue_status["moy"] if ue_status else ""
ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"])
if Se.prev:
if Se.prev_formsemestre:
try:
moy_inter[etud.id] = (moy[etud.id] + prev_moy[etud.id]) / 2.0
except (KeyError, TypeError):

View File

@ -107,9 +107,7 @@ def dict_pvjury(
D = {} # même chose que decisions, mais { etudid : dec }
for etudid in etudids:
etud = Identite.get_etud(etudid)
situation_etud = sco_cursus.get_situation_etud_cursus(
etud.to_dict_scodoc7(), formsemestre_id
)
situation_etud = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
semestre_non_terminal = (
semestre_non_terminal or situation_etud.semestre_non_terminal
)
@ -210,13 +208,13 @@ def dict_pvjury(
if not info:
continue # should not occur
etud = info[0]
if situation_etud.prev and situation_etud.prev_decision:
if situation_etud.prev_formsemestre and situation_etud.prev_decision:
d["prev_decision_sem"] = situation_etud.prev_decision
d["prev_code"] = situation_etud.prev_decision["code"]
d["prev_code_descr"] = _descr_decision_sem(
scu.INSCRIT, situation_etud.prev_decision
)
d["prev"] = situation_etud.prev
d["prev"] = situation_etud.prev_formsemestre.to_dict()
has_prev = True
else:
d["prev_decision_sem"] = None

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.973"
SCOVERSION = "9.6.974"
SCONAME = "ScoDoc"