1
0
forked from ScoDoc/ScoDoc

Exports Apogée: modernise code. Exporte RCUE du BUT: implements #925

This commit is contained in:
Emmanuel Viennet 2024-06-09 15:18:03 +02:00
parent 320cfbebc8
commit 4ae484061e
15 changed files with 274 additions and 299 deletions

View File

@ -113,6 +113,12 @@ class ApcValidationRCUE(db.Model):
"formsemestre_id": self.formsemestre_id, "formsemestre_id": self.formsemestre_id,
} }
def get_codes_apogee(self) -> set[str]:
"""Les codes Apogée associés à cette validation RCUE.
Prend les codes des deux UEs
"""
return self.ue1.get_codes_apogee_rcue() | self.ue2.get_codes_apogee_rcue()
class ApcValidationAnnee(db.Model): class ApcValidationAnnee(db.Model):
"""Validation des années du BUT""" """Validation des années du BUT"""

View File

@ -610,6 +610,41 @@ class FormSemestre(models.ScoDocModel):
) )
) )
@classmethod
def est_in_semestre_scolaire(
cls,
date_debut: datetime.date,
year=False,
periode=None,
mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
) -> bool:
"""Vrai si la date_debut est dans la période indiquée (1,2,0)
du semestre `periode` de l'année scolaire indiquée
(ou, à défaut, de celle en cours).
La période utilise les même conventions que semset["sem_id"];
* 1 : première période
* 2 : deuxième période
* 0 ou période non précisée: annualisé (donc inclut toutes les périodes)
)
"""
if not year:
year = scu.annee_scolaire()
# n'utilise pas le jour pivot
jour_pivot_annee = jour_pivot_periode = 1
# calcule l'année universitaire et la période
sem_annee, sem_periode = cls.comp_periode(
date_debut,
mois_pivot_annee,
mois_pivot_periode,
jour_pivot_annee,
jour_pivot_periode,
)
if periode is None or periode == 0:
return sem_annee == year
return sem_annee == year and sem_periode == periode
def est_terminal(self) -> bool: def est_terminal(self) -> bool:
"Vrai si dernier semestre de son cursus (ou formation mono-semestre)" "Vrai si dernier semestre de son cursus (ou formation mono-semestre)"
return (self.semestre_id < 0) or ( return (self.semestre_id < 0) or (
@ -1225,9 +1260,17 @@ class FormSemestreEtape(db.Model):
"Etape False if code empty" "Etape False if code empty"
return self.etape_apo is not None and (len(self.etape_apo) > 0) return self.etape_apo is not None and (len(self.etape_apo) > 0)
def __eq__(self, other):
if isinstance(other, ApoEtapeVDI):
return self.as_apovdi() == other
return str(self) == str(other)
def __repr__(self): def __repr__(self):
return f"<Etape {self.id} apo={self.etape_apo!r}>" return f"<Etape {self.id} apo={self.etape_apo!r}>"
def __str__(self):
return self.etape_apo or ""
def as_apovdi(self) -> ApoEtapeVDI: def as_apovdi(self) -> ApoEtapeVDI:
return ApoEtapeVDI(self.etape_apo) return ApoEtapeVDI(self.etape_apo)
@ -1381,8 +1424,9 @@ class FormSemestreInscription(db.Model):
def __repr__(self): def __repr__(self):
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={ return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={
self.formsemestre_id} etat={self.etat} { self.formsemestre_id} (S{self.formsemestre.semestre_id}) etat={self.etat} {
('parcours='+str(self.parcour)) if self.parcour else ''}>""" ('parcours="'+str(self.parcour.code)+'"') if self.parcour else ''
} {('etape="'+self.etape+'"') if self.etape else ''}>"""
class NotesSemSet(db.Model): class NotesSemSet(db.Model):

View File

@ -276,6 +276,12 @@ class UniteEns(models.ScoDocModel):
return {x.strip() for x in self.code_apogee.split(",") if x} return {x.strip() for x in self.code_apogee.split(",") if x}
return set() return set()
def get_codes_apogee_rcue(self) -> set[str]:
"""Les codes Apogée RCUE (codés en base comme "VRT1,VRT2")"""
if self.code_apogee_rcue:
return {x.strip() for x in self.code_apogee_rcue.split(",") if x}
return set()
def _parcours_niveaux_ids(self, parcours=list[ApcParcours]) -> set[int]: def _parcours_niveaux_ids(self, parcours=list[ApcParcours]) -> set[int]:
"""set des ids de niveaux communs à tous les parcours listés""" """set des ids de niveaux communs à tous les parcours listés"""
return set.intersection( return set.intersection(

View File

@ -43,12 +43,11 @@ import re
import time import time
from zipfile import ZipFile from zipfile import ZipFile
from flask import send_file from flask import g, send_file
import numpy as np import numpy as np
from app import log from app import log
from app.but import jury_but
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import ( from app.models import (
@ -79,7 +78,6 @@ from app.scodoc.codes_cursus import (
) )
from app.scodoc import sco_cursus from app.scodoc import sco_cursus
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_etud
def _apo_fmt_note(note, fmt="%3.2f"): def _apo_fmt_note(note, fmt="%3.2f"):
@ -99,7 +97,7 @@ class EtuCol:
"""Valeurs colonnes d'un element pour un etudiant""" """Valeurs colonnes d'un element pour un etudiant"""
def __init__(self, nip, apo_elt, init_vals): def __init__(self, nip, apo_elt, init_vals):
pass # XXX pass
ETUD_OK = "ok" ETUD_OK = "ok"
@ -150,9 +148,9 @@ class ApoEtud(dict):
_apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f" _apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f"
) )
# Initialisés par associate_sco: # Initialisés par associate_sco:
self.autre_sem: dict = None self.autre_formsemestre: FormSemestre = None
self.autre_res: NotesTableCompat = None self.autre_res: NotesTableCompat = None
self.cur_sem: dict = None self.cur_formsemestre: FormSemestre = None
self.cur_res: NotesTableCompat = None self.cur_res: NotesTableCompat = None
self.new_cols = {} self.new_cols = {}
"{ col_id : value to record in csv }" "{ col_id : value to record in csv }"
@ -222,7 +220,8 @@ class ApoEtud(dict):
self.new_cols[col_id] = self.cols[col_id] self.new_cols[col_id] = self.cols[col_id]
except KeyError as exc: except KeyError as exc:
raise ScoFormatError( raise ScoFormatError(
f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{col_id}</tt> non déclarée ?""" f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{
col_id}</tt> non déclarée ?"""
) from exc ) from exc
else: else:
try: try:
@ -248,7 +247,7 @@ class ApoEtud(dict):
# codes = set([apo_data.apo_csv.cols[col_id].code for col_id in apo_data.apo_csv.col_ids]) # codes = set([apo_data.apo_csv.cols[col_id].code for col_id in apo_data.apo_csv.col_ids])
# return codes - set(sco_elts) # return codes - set(sco_elts)
def search_elt_in_sem(self, code, sem) -> dict: def search_elt_in_sem(self, code: str, sem: dict) -> dict:
""" """
VET code jury etape (en BUT, le code annuel) VET code jury etape (en BUT, le code annuel)
ELP élément pédagogique: UE, module ELP élément pédagogique: UE, module
@ -263,8 +262,8 @@ class ApoEtud(dict):
sem (dict): semestre dans lequel on cherche l'élément sem (dict): semestre dans lequel on cherche l'élément
Utilise notamment: Utilise notamment:
cur_sem (dict): semestre "courant" pour résultats annuels (VET) cur_formsemestre : semestre "courant" pour résultats annuels (VET)
autre_sem (dict): autre semestre utilisé pour calculer les résultats annuels (VET) autre_formsemestre : autre formsemestre utilisé pour les résultats annuels (VET)
Returns: Returns:
dict: with N, B, J, R keys, ou None si elt non trouvé dict: with N, B, J, R keys, ou None si elt non trouvé
@ -314,10 +313,10 @@ class ApoEtud(dict):
code in {x.strip() for x in sem["elt_annee_apo"].split(",")} code in {x.strip() for x in sem["elt_annee_apo"].split(",")}
): ):
export_res_etape = self.export_res_etape export_res_etape = self.export_res_etape
if (not export_res_etape) and self.cur_sem: if (not export_res_etape) and self.cur_formsemestre:
# exporte toujours le résultat de l'étape si l'étudiant est diplômé # exporte toujours le résultat de l'étape si l'étudiant est diplômé
Se = sco_cursus.get_situation_etud_cursus( Se = sco_cursus.get_situation_etud_cursus(
self.etud, self.cur_sem["formsemestre_id"] self.etud, self.cur_formsemestre.id
) )
export_res_etape = Se.all_other_validated() export_res_etape = Se.all_other_validated()
@ -375,8 +374,20 @@ class ApoEtud(dict):
if module_code_found: if module_code_found:
return VOID_APO_RES return VOID_APO_RES
# RCUE du BUT # RCUE du BUT
deca = jury_but.DecisionsProposeesAnnee(self.etud, formsemestre) if res.is_apc:
for val_rcue in ApcValidationRCUE.query.filter_by(
etudid=etudid, formsemestre_id=sem["formsemestre_id"]
):
if code in val_rcue.get_codes_apogee():
return dict(
N="", # n'exporte pas de moyenne RCUE
B=20,
J="",
R=ScoDocSiteConfig.get_code_apo(val_rcue.code),
M="",
)
# #
return None # element Apogee non trouvé dans ce semestre return None # element Apogee non trouvé dans ce semestre
@ -418,11 +429,10 @@ class ApoEtud(dict):
# #
# XXX cette règle est discutable, à valider # XXX cette règle est discutable, à valider
# log('comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id'])) if not self.cur_formsemestre:
if not self.cur_sem:
# l'étudiant n'a pas de semestre courant ?! # l'étudiant n'a pas de semestre courant ?!
self.log.append("pas de semestre courant") self.log.append("pas de semestre courant")
log(f"comp_elt_annuel: etudid {etudid} has no cur_sem") log(f"comp_elt_annuel: etudid {etudid} has no cur_formsemestre")
return VOID_APO_RES return VOID_APO_RES
if self.is_apc: if self.is_apc:
@ -438,7 +448,7 @@ class ApoEtud(dict):
# ne touche pas aux RATs # ne touche pas aux RATs
return VOID_APO_RES return VOID_APO_RES
if not self.autre_sem: if not self.autre_formsemestre:
# formations monosemestre, ou code VET semestriel, # formations monosemestre, ou code VET semestriel,
# ou jury intermediaire et etudiant non redoublant... # ou jury intermediaire et etudiant non redoublant...
return self.comp_elt_semestre(self.cur_res, cur_decision, etudid) return self.comp_elt_semestre(self.cur_res, cur_decision, etudid)
@ -518,7 +528,7 @@ class ApoEtud(dict):
self.validation_annee_but: ApcValidationAnnee = ( self.validation_annee_but: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by( ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"], etudid=self.etud.id,
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id, referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
).first() ).first()
) )
@ -527,7 +537,7 @@ class ApoEtud(dict):
) )
def etud_set_semestres_de_etape(self, apo_data: "ApoData"): def etud_set_semestres_de_etape(self, apo_data: "ApoData"):
"""Set .cur_sem and .autre_sem et charge les résultats. """Set .cur_formsemestre and .autre_formsemestre et charge les résultats.
Lorsqu'on a une formation semestrialisée mais avec un code étape annuel, Lorsqu'on a une formation semestrialisée mais avec un code étape annuel,
il faut considérer les deux semestres ((S1,S2) ou (S3,S4)) pour calculer il faut considérer les deux semestres ((S1,S2) ou (S3,S4)) pour calculer
le code annuel (VET ou VRT1A (voir elt_annee_apo)). le code annuel (VET ou VRT1A (voir elt_annee_apo)).
@ -535,52 +545,49 @@ class ApoEtud(dict):
Pour les jurys intermediaires (janvier, S1 ou S3): (S2 ou S4) de la même Pour les jurys intermediaires (janvier, S1 ou S3): (S2 ou S4) de la même
étape lors d'une année précédente ? étape lors d'une année précédente ?
Set cur_sem: le semestre "courant" et autre_sem, ou None s'il n'y en a pas. Set cur_formsemestre: le formsemestre "courant"
et autre_formsemestre, ou None s'il n'y en a pas.
""" """
# Cherche le semestre "courant": # Cherche le formsemestre "courant":
cur_sems = [ cur_formsemestres = [
sem formsemestre
for sem in self.etud["sems"] for formsemestre in self.etud.get_formsemestres()
if ( if (
(sem["semestre_id"] == apo_data.cur_semestre_id) (formsemestre.semestre_id == apo_data.cur_semestre_id)
and (apo_data.etape in sem["etapes"]) and (apo_data.etape in formsemestre.etapes)
and ( and (
sco_formsemestre.sem_in_semestre_scolaire( FormSemestre.est_in_semestre_scolaire(
sem, formsemestre.date_debut,
apo_data.annee_scolaire, apo_data.annee_scolaire,
0, # annee complete 0, # annee complete
) )
) )
) )
] ]
if not cur_sems: cur_formsemestre = None
cur_sem = None if cur_formsemestres:
else: # prend le plus récent avec décision
# prend le plus recent avec decision for formsemestre in cur_formsemestres:
cur_sem = None
for sem in cur_sems:
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
has_decision = res.etud_has_decision(self.etud["etudid"]) has_decision = res.etud_has_decision(self.etud.id)
if has_decision: if has_decision:
cur_sem = sem cur_formsemestre = formsemestre
self.cur_res = res self.cur_res = res
break break
if cur_sem is None: if cur_formsemestres is None:
cur_sem = cur_sems[0] # aucun avec décision, prend le plus recent cur_formsemestre = cur_formsemestres[
if res.formsemestre.id == cur_sem["formsemestre_id"]: 0
] # aucun avec décision, prend le plus recent
if res.formsemestre.id == cur_formsemestre.id:
self.cur_res = res self.cur_res = res
else: else:
formsemestre = FormSemestre.query.get_or_404( self.cur_res = res_sem.load_formsemestre_results(cur_formsemestre)
cur_sem["formsemestre_id"]
)
self.cur_res = res_sem.load_formsemestre_results(formsemestre)
self.cur_sem = cur_sem self.cur_formsemestre = cur_formsemestre
if apo_data.cur_semestre_id <= 0: if apo_data.cur_semestre_id <= 0:
# "autre_sem" non pertinent pour sessions sans semestres: # autre_formsemestre non pertinent pour sessions sans semestres:
self.autre_sem = None self.autre_formsemestre = None
self.autre_res = None self.autre_res = None
return return
@ -601,52 +608,49 @@ class ApoEtud(dict):
courant_mois_debut = 1 # ou 2 (fev-jul) courant_mois_debut = 1 # ou 2 (fev-jul)
else: else:
raise ValueError("invalid periode value !") # bug ? raise ValueError("invalid periode value !") # bug ?
courant_date_debut = "%d-%02d-01" % ( courant_date_debut = datetime.date(
courant_annee_debut, day=1, month=courant_mois_debut, year=courant_annee_debut
courant_mois_debut,
) )
else: else:
courant_date_debut = "9999-99-99" courant_date_debut = datetime.date(day=31, month=12, year=9999)
# etud['sems'] est la liste des semestres de l'étudiant, triés par date,
# le plus récemment effectué en tête.
# Cherche les semestres (antérieurs) de l'indice autre de la même étape apogée # Cherche les semestres (antérieurs) de l'indice autre de la même étape apogée
# s'il y en a plusieurs, choisit le plus récent ayant une décision # s'il y en a plusieurs, choisit le plus récent ayant une décision
autres_sems = [] autres_sems = []
for sem in self.etud["sems"]: for formsemestre in self.etud.get_formsemestres():
if ( if (
sem["semestre_id"] == autre_semestre_id formsemestre.semestre_id == autre_semestre_id
and apo_data.etape_apogee in sem["etapes"] and apo_data.etape_apogee in formsemestre.etapes
): ):
if ( if (
sem["date_debut_iso"] < courant_date_debut formsemestre.date_debut < courant_date_debut
): # on demande juste qu'il ait démarré avant ): # on demande juste qu'il ait démarré avant
autres_sems.append(sem) autres_sems.append(formsemestre)
if not autres_sems: if not autres_sems:
autre_sem = None autre_formsemestre = None
elif len(autres_sems) == 1: elif len(autres_sems) == 1:
autre_sem = autres_sems[0] autre_formsemestre = autres_sems[0]
else: else:
autre_sem = None autre_formsemestre = None
for sem in autres_sems: for formsemestre in autres_sems:
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if res.is_apc: if res.is_apc:
has_decision = res.etud_has_decision(self.etud["etudid"]) has_decision = res.etud_has_decision(self.etud.id)
else: else:
has_decision = res.get_etud_decision_sem(self.etud["etudid"]) has_decision = res.get_etud_decision_sem(self.etud.id)
if has_decision: if has_decision:
autre_sem = sem autre_formsemestre = formsemestre
break break
if autre_sem is None: if autre_formsemestre is None:
autre_sem = autres_sems[0] # aucun avec decision, prend le plus recent autre_formsemestre = autres_sems[
0
] # aucun avec decision, prend le plus recent
self.autre_sem = autre_sem self.autre_formsemestre = autre_formsemestre
# Charge les résultats: # Charge les résultats:
if autre_sem: if autre_formsemestre:
formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"]) self.autre_res = res_sem.load_formsemestre_results(self.autre_formsemestre)
self.autre_res = res_sem.load_formsemestre_results(formsemestre)
else: else:
self.autre_res = None self.autre_res = None
@ -873,6 +877,16 @@ class ApoData:
codes_ues = set().union( codes_ues = set().union(
*[ue.get_codes_apogee() for ue in formsemestre.get_ues(with_sport=True)] *[ue.get_codes_apogee() for ue in formsemestre.get_ues(with_sport=True)]
) )
codes_rcues = (
set().union(
*[
ue.get_codes_apogee_rcue()
for ue in formsemestre.get_ues(with_sport=True)
]
)
if self.is_apc
else set()
)
s = set() s = set()
codes_by_sem[sem["formsemestre_id"]] = s codes_by_sem[sem["formsemestre_id"]] = s
for col_id in self.apo_csv.col_ids[4:]: for col_id in self.apo_csv.col_ids[4:]:
@ -885,9 +899,14 @@ class ApoData:
if code in codes_ues: if code in codes_ues:
s.add(code) s.add(code)
continue continue
# associé à un RCUE BUT
if code in codes_rcues:
s.add(code)
continue
# associé à un module: # associé à un module:
if code in codes_modules: if code in codes_modules:
s.add(code) s.add(code)
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem)) # log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
return codes_by_sem return codes_by_sem

View File

@ -139,7 +139,7 @@ class BaseArchiver:
dirs = glob.glob(base + "*") dirs = glob.glob(base + "*")
return [os.path.split(x)[1] for x in dirs] return [os.path.split(x)[1] for x in dirs]
def list_obj_archives(self, oid: int, dept_id: int = None): def list_obj_archives(self, oid: int, dept_id: int = None) -> list[str]:
"""Returns """Returns
:return: list of archive identifiers for this object (paths to non empty dirs) :return: list of archive identifiers for this object (paths to non empty dirs)
""" """

View File

@ -291,14 +291,14 @@ class SituationEtudCursusClassic(SituationEtudCursus):
if s_idx == NO_SEMESTRE_ID or s_idx > 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 return False # n+2 en dehors du parcours
if self._sem_list_validated(set(range(1, s_idx))): if self._sem_list_validated(set(range(1, s_idx))):
# antérieurs validé, teste suivant # antérieurs validés, teste suivant
n1 = s_idx + 1 n1 = s_idx + 1
for sem in self.get_semestres(): for formsemestre in self.formsemestres:
if ( if (
sem["semestre_id"] == n1 formsemestre.semestre_id == n1
and sem["formation_code"] == self.formation.formation_code and formsemestre.formation.formation_code
== self.formation.formation_code
): ):
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results( nt: NotesTableCompat = res_sem.load_formsemestre_results(
formsemestre formsemestre
) )

View File

@ -84,6 +84,7 @@ _ueEditor = ndb.EditableTable(
"ects", "ects",
"is_external", "is_external",
"code_apogee", "code_apogee",
"code_apogee_rcue",
"coefficient", "coefficient",
"coef_rcue", "coef_rcue",
"color", "color",
@ -425,6 +426,20 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"max_length": APO_CODE_STR_LEN, "max_length": APO_CODE_STR_LEN,
}, },
), ),
]
if is_apc:
form_descr += [
(
"code_apogee_rcue",
{
"title": "Code Apogée du RCUE",
"size": 25,
"explanation": "(optionnel) code(s) élément pédagogique Apogée du RCUE",
"max_length": APO_CODE_STR_LEN,
},
),
]
form_descr += [
( (
"is_external", "is_external",
{ {

View File

@ -247,9 +247,7 @@ def apo_csv_check_etape(semset, set_nips, etape_apo):
return nips_ok, apo_nips, nips_no_apo, nips_no_sco, maq_elems, sem_elems return nips_ok, apo_nips, nips_no_apo, nips_no_sco, maq_elems, sem_elems
def apo_csv_semset_check( def apo_csv_semset_check(semset, allow_missing_apo=False, allow_missing_csv=False):
semset, allow_missing_apo=False, allow_missing_csv=False
): # was apo_csv_check
""" """
check students in stored maqs vs students in semset check students in stored maqs vs students in semset
Cas à détecter: Cas à détecter:
@ -346,120 +344,3 @@ def apo_csv_retreive_etuds_by_nip(semset, nips):
etuds[nip] = apo_etuds_by_nips.get(nip, {"nip": nip, "etape_apo": "?"}) etuds[nip] = apo_etuds_by_nips.get(nip, {"nip": nip, "etape_apo": "?"})
return etuds return etuds
"""
Tests:
from debug import *
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_formsemestre
from app.scodoc.sco_etape_apogee import *
from app.scodoc.sco_apogee_csv import *
from app.scodoc.sco_semset import *
app.set_sco_dept('RT')
csv_data = open('/opt/misc/VDTRT_V1RT.TXT').read()
annee_scolaire=2015
sem_id=1
apo_data = sco_apogee_csv.ApoData(csv_data, periode=sem_id)
print apo_data.etape_apogee
apo_data.setup()
e = apo_data.etuds[0]
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
e.associate_sco( apo_data)
print apo_csv_list_stored_archives()
# apo_csv_store(csv_data, annee_scolaire, sem_id)
groups_infos = sco_groups_view.DisplayedGroupsInfos( [sco_groups.get_default_group(formsemestre_id)], formsemestre_id=formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
#
s = SemSet('NSS29902')
apo_data = sco_apogee_csv.ApoData(open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2015-2/2016-07-10-11-26-15/V1RT.csv').read(), periode=1)
# cas Tiziri K. (inscrite en S1, démission en fin de S1, pas inscrite en S2)
# => pas de décision, ce qui est voulu (?)
#
apo_data.setup()
e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0]
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
e.associate_sco(apo_data)
self=e
col_id='apoL_c0129'
# --
from app.scodoc import sco_portal_apogee
_ = go_dept(app, 'GEA').Notes
#csv_data = sco_portal_apogee.get_maquette_apogee(etape='V1GE', annee_scolaire=2015)
csv_data = open('/tmp/V1GE.txt').read()
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
# ------
# les elements inconnus:
from debug import *
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_formsemestre
from app.scodoc.sco_etape_apogee import *
from app.scodoc.sco_apogee_csv import *
from app.scodoc.sco_semset import *
_ = go_dept(app, 'RT').Notes
csv_data = open('/opt/misc/V2RT.csv').read()
annee_scolaire=2015
sem_id=1
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
print apo_data.etape_apogee
apo_data.setup()
for e in apo_data.etuds:
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
e.associate_sco(apo_data)
# ------
# test export jury intermediaire
from debug import *
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_formsemestre
from app.scodoc.sco_etape_apogee import *
from app.scodoc.sco_apogee_csv import *
from app.scodoc.sco_semset import *
_ = go_dept(app, 'CJ').Notes
csv_data = open('/opt/scodoc/var/scodoc/archives/apo_csv/CJ/2016-1/2017-03-06-21-46-32/V1CJ.csv').read()
annee_scolaire=2016
sem_id=1
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
print apo_data.etape_apogee
apo_data.setup()
e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0] #
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
e.associate_sco(apo_data)
self=e
sco_elts = {}
col_id='apoL_c0001'
code = apo_data.cols[col_id]['Code'] # 'V1RT'
sem = apo_data.sems_periode[0] # le S1
"""

View File

@ -125,14 +125,19 @@ def apo_semset_maq_status(
H.append("""<p><em>Aucune maquette chargée</em></p>""") H.append("""<p><em>Aucune maquette chargée</em></p>""")
# Upload fichier: # Upload fichier:
H.append( H.append(
"""<form id="apo_csv_add" action="view_apo_csv_store" method="post" enctype="multipart/form-data"> f"""<form id="apo_csv_add" action="view_apo_csv_store"
Charger votre fichier maquette Apogée: method="post" enctype="multipart/form-data"
style="margin-bottom: 8px;"
>
<div style="margin-top: 12px; margin-bottom: 8px;">
{'Charger votre fichier' if tab_archives.is_empty() else 'Ajouter un autre fichier'}
maquette Apogée:
</div>
<input type="file" size="30" name="csvfile"/> <input type="file" size="30" name="csvfile"/>
<input type="hidden" name="semset_id" value="%s"/> <input type="hidden" name="semset_id" value="{semset_id}"/>
<input type="submit" value="Ajouter ce fichier"/> <input type="submit" value="Ajouter ce fichier"/>
<input type="checkbox" name="autodetect" checked/>autodétecter encodage</input> <input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
</form>""" </form>"""
% (semset_id,)
) )
# Récupération sur portail: # Récupération sur portail:
maquette_url = sco_portal_apogee.get_maquette_url() maquette_url = sco_portal_apogee.get_maquette_url()
@ -335,7 +340,7 @@ def apo_semset_maq_status(
missing = maq_elems - sem_elems missing = maq_elems - sem_elems
H.append('<div id="apo_elements">') H.append('<div id="apo_elements">')
H.append( H.append(
'<p>Elements Apogée: <span class="apo_elems">%s</span></p>' '<p>Élements Apogée: <span class="apo_elems">%s</span></p>'
% ", ".join( % ", ".join(
[ [
e if not e in missing else '<span class="missing">' + e + "</span>" e if not e in missing else '<span class="missing">' + e + "</span>"
@ -351,7 +356,7 @@ def apo_semset_maq_status(
] ]
H.append( H.append(
f"""<div class="apo_csv_status_missing_elems"> f"""<div class="apo_csv_status_missing_elems">
<span class="fontred">Elements Apogée absents dans ScoDoc: </span> <span class="fontred">Élements Apogée absents dans ScoDoc: </span>
<span class="apo_elems fontred">{ <span class="apo_elems fontred">{
", ".join(sorted(missing)) ", ".join(sorted(missing))
}</span> }</span>
@ -442,11 +447,11 @@ def table_apo_csv_list(semset):
annee_scolaire = semset["annee_scolaire"] annee_scolaire = semset["annee_scolaire"]
sem_id = semset["sem_id"] sem_id = semset["sem_id"]
T = sco_etape_apogee.apo_csv_list_stored_archives( rows = sco_etape_apogee.apo_csv_list_stored_archives(
annee_scolaire, sem_id, etapes=semset.list_etapes() annee_scolaire, sem_id, etapes=semset.list_etapes()
) )
for t in T: for t in rows:
# Ajoute qq infos pour affichage: # Ajoute qq infos pour affichage:
csv_data = sco_etape_apogee.apo_csv_get(t["etape_apo"], annee_scolaire, sem_id) csv_data = sco_etape_apogee.apo_csv_get(t["etape_apo"], annee_scolaire, sem_id)
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"]) apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
@ -484,7 +489,7 @@ def table_apo_csv_list(semset):
"date_str": "Enregistré le", "date_str": "Enregistré le",
}, },
columns_ids=columns_ids, columns_ids=columns_ids,
rows=T, rows=rows,
html_class="table_leftalign apo_maq_list", html_class="table_leftalign apo_maq_list",
html_sortable=True, html_sortable=True,
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id), # base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),

View File

@ -93,7 +93,7 @@ import json
from flask import url_for, g from flask import url_for, g
from app.scodoc.sco_portal_apogee import get_inscrits_etape from app.scodoc import sco_portal_apogee
from app import log from app import log
from app.scodoc.sco_utils import annee_scolaire_debut from app.scodoc.sco_utils import annee_scolaire_debut
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
@ -136,11 +136,16 @@ class DataEtudiant(object):
self.etudid = etudid self.etudid = etudid
self.data_apogee = None self.data_apogee = None
self.data_scodoc = None self.data_scodoc = None
self.etapes = set() # l'ensemble des étapes où il est inscrit self.etapes = set()
self.semestres = set() # l'ensemble des formsemestre_id où il est inscrit "l'ensemble des étapes où il est inscrit"
self.tags = set() # les anomalies relevées self.semestres = set()
self.ind_row = "-" # là où il compte dans les effectifs (ligne et colonne) "l'ensemble des formsemestre_id où il est inscrit"
self.tags = set()
"les anomalies relevées"
self.ind_row = "-"
"ligne où il compte dans les effectifs"
self.ind_col = "-" self.ind_col = "-"
"colonne où il compte dans les effectifs"
def add_etape(self, etape): def add_etape(self, etape):
self.etapes.add(etape) self.etapes.add(etape)
@ -163,9 +168,9 @@ class DataEtudiant(object):
def set_ind_col(self, indicatif): def set_ind_col(self, indicatif):
self.ind_col = indicatif self.ind_col = indicatif
def get_identity(self): def get_identity(self) -> str:
""" """
Calcul le nom/prénom de l'étudiant (données ScoDoc en priorité, sinon données Apogée) Calcule le nom/prénom de l'étudiant (données ScoDoc en priorité, sinon données Apogée)
:return: L'identité calculée :return: L'identité calculée
""" """
if self.data_scodoc is not None: if self.data_scodoc is not None:
@ -176,9 +181,12 @@ class DataEtudiant(object):
def _help() -> str: def _help() -> str:
return """ return """
<div id="export_help" class="pas_help"> <span>Explications sur les tableaux des effectifs et liste des <div id="export_help" class="pas_help">
étudiants</span> <span>Explications sur les tableaux des effectifs
<div> <p>Le tableau des effectifs présente le nombre d'étudiants selon deux critères:</p> et liste des étudiants</span>
<div>
<p>Le tableau des effectifs présente le nombre d'étudiants selon deux critères:
</p>
<ul> <ul>
<li>En colonne le statut de l'étudiant par rapport à Apogée: <li>En colonne le statut de l'étudiant par rapport à Apogée:
<ul> <ul>
@ -406,7 +414,8 @@ class EtapeBilan:
for key_etape in self.etapes: for key_etape in self.etapes:
annee_apogee, etapestr = key_to_values(key_etape) annee_apogee, etapestr = key_to_values(key_etape)
self.etu_etapes[key_etape] = set() self.etu_etapes[key_etape] = set()
for etud in get_inscrits_etape(etapestr, annee_apogee): # get_inscrits_etape interroge portail Apo:
for etud in sco_portal_apogee.get_inscrits_etape(etapestr, annee_apogee):
key_etu = self.register_etud_apogee(etud, key_etape) key_etu = self.register_etud_apogee(etud, key_etape)
self.etu_etapes[key_etape].add(key_etu) self.etu_etapes[key_etape].add(key_etu)
@ -444,7 +453,6 @@ class EtapeBilan:
data_etu = self.etudiants[key_etu] data_etu = self.etudiants[key_etu]
ind_col = "-" ind_col = "-"
ind_row = "-" ind_row = "-"
# calcul de la colonne # calcul de la colonne
if len(data_etu.etapes) == 1: if len(data_etu.etapes) == 1:
ind_col = self.indicatifs[list(data_etu.etapes)[0]] ind_col = self.indicatifs[list(data_etu.etapes)[0]]
@ -478,32 +486,34 @@ class EtapeBilan:
affichage de l'html affichage de l'html
:return: Le code html à afficher :return: Le code html à afficher
""" """
if not sco_portal_apogee.has_portal():
return """<div id="synthese" class="semset_description">
<em>Pas de portail Apogée configuré</em>
</div>"""
self.load_listes() # chargement des données self.load_listes() # chargement des données
self.dispatch() # analyse et répartition self.dispatch() # analyse et répartition
# calcul de la liste des colonnes et des lignes de la table des effectifs # calcul de la liste des colonnes et des lignes de la table des effectifs
self.all_rows_str = "'" + ",".join(["." + r for r in self.all_rows_ind]) + "'" self.all_rows_str = "'" + ",".join(["." + r for r in self.all_rows_ind]) + "'"
self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'" self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'"
H = [ return f"""
"""<div id="synthese" class="semset_description"> <div id="synthese" class="semset_description">
<details open="true"> <details open="true">
<summary><b>Tableau des effectifs</b> <summary><b>Tableau des effectifs</b>
</summary> </summary>
""", {self._diagtable()}
self._diagtable(), </details>
"""</details>""", {self.display_tags()}
self.display_tags(), <details open="true">
"""<details open="true"> <summary>
<summary><b id="effectifs">Liste des étudiants <span id="compte"></span></b> <b id="effectifs">Liste des étudiants <span id="compte"></span></b>
</summary> </summary>
""", {entete_liste_etudiant()}
entete_liste_etudiant(), {self.table_effectifs()}
self.table_effectifs(), </details>
"""</details>""", {_help()}
_help(), </div>
] """
return "\n".join(H)
def _inc_count(self, ind_row, ind_col): def _inc_count(self, ind_row, ind_col):
if (ind_row, ind_col) not in self.repartition: if (ind_row, ind_col) not in self.repartition:
@ -692,26 +702,34 @@ class EtapeBilan:
return "\n".join(H) return "\n".join(H)
@staticmethod @staticmethod
def link_etu(etudid, nom): def link_etu(etudid, nom) -> str:
return '<a class="stdlink" href="%s">%s</a>' % ( "Lien html vers fiche de l'étudiant"
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid), return f"""<a class="stdlink" href="{
nom, url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
) }">{nom}</a>"""
def link_semestre(self, semestre, short=False): def link_semestre(self, semestre, short=False) -> str:
if short: "Lien html vers tableau de bord semestre"
return ( key = "session_id" if short else "titremois"
'<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(' sem = self.semestres[semestre]
"formsemestre_id)s</a> " % self.semestres[semestre] return f"""<a class="stdlink" href="{
) url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
else: formsemestre_id=sem['formsemestre_id']
return ( )}">{sem[key]}</a>
'<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s' """
" %(mois_debut)s - %(mois_fin)s)</a>" % self.semestres[semestre]
)
def table_effectifs(self): def table_effectifs(self) -> str:
H = [] "Table html donnant les étudiants dans chaque semestre"
H = [
"""
<style>
table#apo-detail td.semestre {
white-space: nowrap;
word-break: normal;
}
</style>
"""
]
col_ids = ["tag", "etudiant", "prenom", "nip", "semestre", "apogee", "annee"] col_ids = ["tag", "etudiant", "prenom", "nip", "semestre", "apogee", "annee"]
titles = { titles = {
@ -766,6 +784,7 @@ class EtapeBilan:
titles, titles,
html_class="table_leftalign", html_class="table_leftalign",
html_sortable=True, html_sortable=True,
html_with_td_classes=True,
table_id="apo-detail", table_id="apo-detail",
).gen(fmt="html") ).gen(fmt="html")
) )

View File

@ -143,6 +143,7 @@ def formation_export_dict(
if not export_codes_apo: if not export_codes_apo:
ue_dict.pop("code_apogee", None) ue_dict.pop("code_apogee", None)
ue_dict.pop("code_apogee_rcue", None)
if ue_dict.get("ects") is None: if ue_dict.get("ects") is None:
ue_dict.pop("ects", None) ue_dict.pop("ects", None)
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id}) mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})

View File

@ -419,49 +419,23 @@ def sem_set_responsable_name(sem):
) )
def sem_in_semestre_scolaire( def sem_in_annee_scolaire(sem: dict, year=False): # OBSOLETE
sem,
year=False,
periode=None,
mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
) -> bool:
"""Vrai si la date du début du semestre est dans la période indiquée (1,2,0)
du semestre `periode` de l'année scolaire indiquée
(ou, à défaut, de celle en cours).
La période utilise les même conventions que semset["sem_id"];
* 1 : première période
* 2 : deuxième période
* 0 ou période non précisée: annualisé (donc inclut toutes les périodes)
)
"""
if not year:
year = scu.annee_scolaire()
# n'utilise pas le jour pivot
jour_pivot_annee = jour_pivot_periode = 1
# calcule l'année universitaire et la période
sem_annee, sem_periode = FormSemestre.comp_periode(
datetime.datetime.fromisoformat(sem["date_debut_iso"]),
mois_pivot_annee,
mois_pivot_periode,
jour_pivot_annee,
jour_pivot_periode,
)
if periode is None or periode == 0:
return sem_annee == year
return sem_annee == year and sem_periode == periode
def sem_in_annee_scolaire(sem, year=False):
"""Test si sem appartient à l'année scolaire year (int). """Test si sem appartient à l'année scolaire year (int).
N'utilise que la date de début, pivot au 1er août. N'utilise que la date de début, pivot au 1er août.
Si année non specifiée, année scolaire courante Si année non specifiée, année scolaire courante
""" """
return sem_in_semestre_scolaire(sem, year, periode=0) return FormSemestre.est_in_semestre_scolaire(
datetime.date.fromisoformat(sem["date_debut_iso"]), year, periode=0
)
def sem_est_courant(sem): # -> FormSemestre.est_courant def sem_in_semestre_scolaire(sem, year=False, periode=None): # OBSOLETE
return FormSemestre.est_in_semestre_scolaire(
datetime.date.fromisoformat(sem["date_debut_iso"]), year, periode=periode
)
def sem_est_courant(sem: dict): # -> FormSemestre.est_courant
"""Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses)""" """Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses)"""
now = time.strftime("%Y-%m-%d") now = time.strftime("%Y-%m-%d")
debut = ndb.DateDMYtoISO(sem["date_debut"]) debut = ndb.DateDMYtoISO(sem["date_debut"])

View File

@ -438,12 +438,13 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"elt_sem_apo", "elt_sem_apo",
{ {
"size": 32, "size": 32,
"title": "Element(s) Apogée:", "title": "Element(s) Apogée sem.:",
"explanation": "associé(s) au résultat du semestre (ex: VRTW1). Inutile en BUT. Séparés par des virgules.", "explanation": "associé(s) au résultat du semestre (ex: VRTW1). Inutile en BUT. Séparés par des virgules.",
"allow_null": not sco_preferences.get_preference( "allow_null": not sco_preferences.get_preference(
"always_require_apo_sem_codes" "always_require_apo_sem_codes"
) )
or (formsemestre and formsemestre.modalite == "EXT"), or (formsemestre and formsemestre.modalite == "EXT")
or (formsemestre.formation.is_apc()),
}, },
) )
) )
@ -452,7 +453,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"elt_annee_apo", "elt_annee_apo",
{ {
"size": 32, "size": 32,
"title": "Element(s) Apogée:", "title": "Element(s) Apogée année:",
"explanation": "associé(s) au résultat de l'année (ex: VRT1A). Séparés par des virgules.", "explanation": "associé(s) au résultat de l'année (ex: VRT1A). Séparés par des virgules.",
"allow_null": not sco_preferences.get_preference( "allow_null": not sco_preferences.get_preference(
"always_require_apo_sem_codes" "always_require_apo_sem_codes"

View File

@ -381,7 +381,6 @@ class SemSet(dict):
(actif seulement si un portail est configuré) (actif seulement si un portail est configuré)
""" """
return self.bilan.html_diagnostic() return self.bilan.html_diagnostic()
return ""
def get_semsets_list(): def get_semsets_list():

View File

@ -4165,6 +4165,11 @@ div.apo_csv_list {
border: 1px dashed rgb(150, 10, 40); border: 1px dashed rgb(150, 10, 40);
} }
table#apo_csv_list td {
white-space: nowrap;
word-break: no-wrap;
}
#apo_csv_download { #apo_csv_download {
margin-top: 5px; margin-top: 5px;
} }