ScoDoc/app/scodoc/sco_apogee_csv.py

1109 lines
42 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
##############################################################################
#
# Gestion scolarite IUT
#
2023-01-02 13:16:27 +01:00
# Copyright (c) 1999 - 2023 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
#
##############################################################################
"""Exportation des résultats des étudiants vers Apogée.
2021-02-01 23:54:46 +01:00
Ce code a été au départ inspiré par les travaux de Damien Mascré, scodoc2apogee (en Java).
2020-09-26 16:19:37 +02:00
A utiliser en fin de semestre, après les jury.
On communique avec Apogée via des fichiers CSV.
XXX A vérifier: AJAC car 1 sem. validé et pas de NAR
2020-09-26 16:19:37 +02:00
"""
2021-02-01 23:54:46 +01:00
import datetime
from functools import reduce
import functools
import io
2021-02-01 23:54:46 +01:00
import os
import pprint
import re
import time
2020-09-26 16:19:37 +02:00
from zipfile import ZipFile
from flask import send_file
2022-08-25 18:43:24 +02:00
import numpy as np
2020-09-26 16:19:37 +02:00
from app import log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite, ApcValidationAnnee
from app.models.config import ScoDocSiteConfig
from app.scodoc.sco_apogee_reader import (
APO_DECIMAL_SEP,
ApoCSVReadWrite,
ApoEtudTuple,
)
import app.scodoc.sco_utils as scu
2021-12-03 14:13:49 +01:00
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
from app.scodoc.gen_tables import GenTable
2021-07-10 13:55:35 +02:00
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.codes_cursus import code_semestre_validant
from app.scodoc.codes_cursus import (
DEF,
DEM,
NAR,
RAT,
)
2022-07-07 16:24:52 +02:00
from app.scodoc import sco_cursus
from app.scodoc import sco_formsemestre
from app.scodoc import sco_etud
2020-09-26 16:19:37 +02:00
def _apo_fmt_note(note, fmt="%3.2f"):
2020-09-26 16:19:37 +02:00
"Formatte une note pour Apogée (séparateur décimal: ',')"
# if not note and isinstance(note, float): changé le 31/1/2022, étrange ?
# return ""
2020-09-26 16:19:37 +02:00
try:
val = float(note)
except ValueError:
return ""
2022-08-25 18:43:24 +02:00
if np.isnan(val):
return ""
return (fmt % val).replace(".", APO_DECIMAL_SEP)
2020-09-26 16:19:37 +02:00
class EtuCol:
2020-09-26 16:19:37 +02:00
"""Valeurs colonnes d'un element pour un etudiant"""
def __init__(self, nip, apo_elt, init_vals):
pass # XXX
ETUD_OK = "ok"
ETUD_ORPHELIN = "orphelin"
ETUD_NON_INSCRIT = "non_inscrit"
VOID_APO_RES = dict(N="", B="", J="", R="", M="")
class ApoEtud(dict):
"""Étudiant Apogee:"""
2020-09-26 16:19:37 +02:00
def __init__(
self,
apo_etud_tuple: ApoEtudTuple,
2020-09-26 16:19:37 +02:00
export_res_etape=True,
export_res_sem=True,
export_res_ues=True,
export_res_modules=True,
export_res_sdj=True,
export_res_rat=True,
):
self["nip"] = apo_etud_tuple.nip
self["nom"] = apo_etud_tuple.nom
self["prenom"] = apo_etud_tuple.prenom
self["naissance"] = apo_etud_tuple.naissance
self.cols = apo_etud_tuple.cols
2022-07-02 11:17:04 +02:00
"{ col_id : value } colid = 'apoL_c0001'"
self.is_apc = None
"Vrai si BUT"
2022-07-02 11:17:04 +02:00
self.col_elts = {}
"{'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}"
self.etud: Identite = None
"etudiant ScoDoc associé"
2020-09-26 16:19:37 +02:00
self.etat = None # ETUD_OK, ...
self.is_nar = False
2022-07-02 11:17:04 +02:00
"True si NARé dans un semestre"
2020-09-26 16:19:37 +02:00
self.log = []
self.has_logged_no_decision = False
self.export_res_etape = export_res_etape # VET, ...
self.export_res_sem = export_res_sem # elt_sem_apo
self.export_res_ues = export_res_ues
self.export_res_modules = export_res_modules
self.export_res_sdj = export_res_sdj
"export meme si pas de decision de jury"
2020-09-26 16:19:37 +02:00
self.export_res_rat = export_res_rat
self.fmt_note = functools.partial(
_apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f"
)
2023-04-19 11:51:58 +02:00
# Initialisés par associate_sco:
self.autre_sem: dict = None
self.autre_res: NotesTableCompat = None
self.cur_sem: dict = None
self.cur_res: NotesTableCompat = None
self.new_cols = {}
"{ col_id : value to record in csv }"
# Pour le BUT:
self.validation_annee_but: ApcValidationAnnee = None
"validation de jury annuelle BUT, ou None"
2020-09-26 16:19:37 +02:00
def __repr__(self):
2022-07-02 11:17:04 +02:00
return f"""ApoEtud( nom='{self["nom"]}', nip='{self["nip"]}' )"""
2020-09-26 16:19:37 +02:00
def lookup_scodoc(self, etape_formsemestre_ids):
"""Cherche l'étudiant ScoDoc associé à cet étudiant Apogée.
S'il n'est pas trouvé (état "orphelin", dans Apo mais pas chez nous),
met .etud à None.
Sinon, cherche le semestre, et met l'état à ETUD_OK ou ETUD_NON_INSCRIT.
"""
2022-07-02 11:17:04 +02:00
# futur: #WIP
# etud: Identite = Identite.query.filter_by(code_nip=self["nip"], dept_id=g.scodoc_dept_id).first()
2022-07-02 11:17:04 +02:00
# self.etud = etud
etuds = sco_etud.get_etud_info(code_nip=self["nip"], filled=True)
2020-09-26 16:19:37 +02:00
if not etuds:
# pas dans ScoDoc
self.etud = None
self.log.append("non inscrit dans ScoDoc")
self.etat = ETUD_ORPHELIN
else:
2022-07-02 11:17:04 +02:00
# futur: #WIP
# formsemestre_ids = {
# ins.formsemestre_id for ins in etud.formsemestre_inscriptions
# }
# in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
2020-09-26 16:19:37 +02:00
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"]}
2022-07-02 11:17:04 +02:00
in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
if not in_formsemestre_ids:
2020-09-26 16:19:37 +02:00
self.log.append(
"connu dans ScoDoc, mais pas inscrit dans un semestre de cette étape"
)
self.etat = ETUD_NON_INSCRIT
else:
self.etat = ETUD_OK
2022-07-02 11:17:04 +02:00
def associate_sco(self, apo_data: "ApoData"):
2020-09-26 16:19:37 +02:00
"""Recherche les valeurs des éléments Apogée pour cet étudiant
Set .new_cols
"""
self.col_elts = {} # {'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}
if self.etat is None:
self.lookup_scodoc(apo_data.etape_formsemestre_ids)
2020-09-26 16:19:37 +02:00
if self.etat != ETUD_OK:
self.new_cols = (
self.cols
) # etudiant inconnu, recopie les valeurs existantes dans Apo
else:
sco_elts = {} # valeurs trouvées dans ScoDoc code : { N, B, J, R }
for col_id in apo_data.apo_csv.col_ids[4:]:
code = apo_data.apo_csv.cols[col_id]["Code"] # 'V1RT'
2023-04-19 11:51:58 +02:00
elt = sco_elts.get(code, None)
# elt est {'R': ADM, 'J': '', 'B': 20, 'N': '12.14'}
if elt is None: # pas déjà trouvé
self.etud_set_semestres_de_etape(apo_data)
2020-09-26 16:19:37 +02:00
for sem in apo_data.sems_etape:
2023-04-19 11:51:58 +02:00
elt = self.search_elt_in_sem(code, sem)
if elt is not None:
sco_elts[code] = elt
2020-09-26 16:19:37 +02:00
break
2023-04-19 11:51:58 +02:00
self.col_elts[code] = elt
if elt is None:
2020-09-26 16:19:37 +02:00
self.new_cols[col_id] = self.cols[col_id]
else:
try:
self.new_cols[col_id] = sco_elts[code][
apo_data.apo_csv.cols[col_id]["Type Rés."]
2020-09-26 16:19:37 +02:00
]
2022-07-02 11:17:04 +02:00
except KeyError as exc:
2020-09-26 16:19:37 +02:00
log(
f"""associate_sco: missing key, etud={self}\ncode='{
code}'\netape='{apo_data.etape_apogee}'"""
2020-09-26 16:19:37 +02:00
)
raise ScoValueError(
2022-07-02 11:17:04 +02:00
f"""L'élément {code} n'a pas de résultat: peut-être une erreur
dans les codes sur le programme pédagogique
(vérifier qu'il est bien associé à une UE ou semestre)?"""
) from exc
2020-09-26 16:19:37 +02:00
# recopie les 4 premieres colonnes (nom, ..., naissance):
for col_id in apo_data.apo_csv.col_ids[:4]:
2020-09-26 16:19:37 +02:00
self.new_cols[col_id] = self.cols[col_id]
2021-02-01 23:54:46 +01:00
# def unassociated_codes(self, apo_data):
# "list of apo elements for this student without a value in ScoDoc"
# codes = set([apo_data.apo_csv.cols[col_id].code for col_id in apo_data.apo_csv.col_ids])
2021-02-01 23:54:46 +01:00
# return codes - set(sco_elts)
2020-09-26 16:19:37 +02:00
2023-04-19 11:51:58 +02:00
def search_elt_in_sem(self, code, sem) -> dict:
2020-09-26 16:19:37 +02:00
"""
VET code jury etape (en BUT, le code annuel)
2020-09-26 16:19:37 +02:00
ELP élément pédagogique: UE, module
Autres éléments: résultats du semestre ou de l'année scolaire:
=> VRTW1: code additionnel au semestre ("code élement semestre", elt_sem_apo)
=> VRT1A: le même que le VET: ("code élement annuel", elt_annee_apo)
2021-01-01 18:40:47 +01:00
Attention, si le semestre couvre plusieurs étapes, indiquer les codes des éléments,
2020-09-26 16:19:37 +02:00
séparés par des virgules.
Args:
code (str): code apo de l'element cherché
sem (dict): semestre dans lequel on cherche l'élément
cur_sem (dict): semestre "courant" pour résultats annuels (VET)
2023-04-19 11:51:58 +02:00
autre_sem (dict): autre semestre utilisé pour calculer les résultats annuels (VET)
2021-01-01 18:40:47 +01:00
2020-09-26 16:19:37 +02:00
Returns:
dict: with N, B, J, R keys, ou None si elt non trouvé
"""
etudid = self.etud["etudid"]
2023-04-19 11:51:58 +02:00
if not self.cur_res:
log("search_elt_in_sem: no cur_res !")
return None
if sem["formsemestre_id"] == self.cur_res.formsemestre.id:
res = self.cur_res
elif (
self.autre_res and sem["formsemestre_id"] == self.autre_res.formsemestre.id
):
res = self.autre_res
else:
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
2023-04-19 11:51:58 +02:00
if etudid not in res.identdict:
2020-09-26 16:19:37 +02:00
return None # etudiant non inscrit dans ce semestre
2023-04-19 11:51:58 +02:00
if not self.export_res_sdj and not res.etud_has_decision(etudid):
2020-09-26 16:19:37 +02:00
# pas de decision de jury, on n'enregistre rien
# (meme si démissionnaire)
if not self.has_logged_no_decision:
self.log.append("Pas de décision (export désactivé)")
2020-09-26 16:19:37 +02:00
self.has_logged_no_decision = True
return VOID_APO_RES
2023-04-19 11:51:58 +02:00
if res.is_apc: # export BUT
self._but_load_validation_annuelle()
else:
decision = res.get_etud_decision_sem(etudid)
if decision and decision["code"] == NAR:
self.is_nar = True
2023-04-19 11:51:58 +02:00
# Element semestre: (non BUT donc)
if code in {x.strip() for x in sem["elt_sem_apo"].split(",")}:
if self.export_res_sem:
return self.comp_elt_semestre(res, decision, etudid)
else:
return VOID_APO_RES
2020-09-26 16:19:37 +02:00
# Element etape (annuel ou non):
if sco_formsemestre.sem_has_etape(sem, code) or (
code in {x.strip() for x in sem["elt_annee_apo"].split(",")}
2020-09-26 16:19:37 +02:00
):
export_res_etape = self.export_res_etape
2023-04-19 11:51:58 +02:00
if (not export_res_etape) and self.cur_sem:
2020-09-26 16:19:37 +02:00
# exporte toujours le résultat de l'étape si l'étudiant est diplômé
2022-07-07 16:24:52 +02:00
Se = sco_cursus.get_situation_etud_cursus(
2023-04-19 11:51:58 +02:00
self.etud, self.cur_sem["formsemestre_id"]
2020-09-26 16:19:37 +02:00
)
export_res_etape = Se.all_other_validated()
if export_res_etape:
2023-04-19 11:51:58 +02:00
return self.comp_elt_annuel(etudid)
2020-09-26 16:19:37 +02:00
else:
self.log.append("export étape désactivé")
2020-09-26 16:19:37 +02:00
return VOID_APO_RES
# Elements UE
2023-04-19 11:51:58 +02:00
decisions_ue = res.get_etud_decisions_ue(etudid)
for ue in res.get_ues_stat_dict():
if ue["code_apogee"] and code in {
x.strip() for x in ue["code_apogee"].split(",")
}:
2020-09-26 16:19:37 +02:00
if self.export_res_ues:
if decisions_ue and ue["ue_id"] in decisions_ue:
2023-04-19 11:51:58 +02:00
ue_status = res.get_etud_ue_status(etudid, ue["ue_id"])
2020-09-26 16:19:37 +02:00
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
return dict(
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
2020-09-26 16:19:37 +02:00
B=20,
J="",
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
2020-09-26 16:19:37 +02:00
M="",
)
else:
return VOID_APO_RES
else:
return VOID_APO_RES
# Elements Modules
2023-04-19 11:51:58 +02:00
modimpls = res.get_modimpls_dict()
2020-09-26 16:19:37 +02:00
module_code_found = False
for modimpl in modimpls:
module = modimpl["module"]
if module["code_apogee"] and code in {
x.strip() for x in module["code_apogee"].split(",")
}:
2023-04-19 11:51:58 +02:00
n = res.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
2020-09-26 16:19:37 +02:00
if n != "NI" and self.export_res_modules:
return dict(N=self.fmt_note(n), B=20, J="", R="")
2020-09-26 16:19:37 +02:00
else:
module_code_found = True
if module_code_found:
return VOID_APO_RES
#
return None # element Apogee non trouvé dans ce semestre
2023-04-17 12:10:17 +02:00
def comp_elt_semestre(self, nt: NotesTableCompat, decision: dict, etudid: int):
"""Calcul résultat apo semestre.
Toujours vide pour en BUT/APC.
"""
2023-04-19 11:51:58 +02:00
if self.is_apc: # garde fou: pas de code semestre en APC !
return dict(N="", B=20, J="", R="", M="")
2022-02-26 20:22:18 +01:00
if decision is None:
etud = Identite.get_etud(etudid)
2022-02-26 20:22:18 +01:00
nomprenom = etud.nomprenom if etud else "(inconnu)"
raise ScoValueError(
f"decision absente pour l'étudiant {nomprenom} ({etudid})"
)
2020-09-26 16:19:37 +02:00
# resultat du semestre
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
2020-09-26 16:19:37 +02:00
note = nt.get_etud_moy_gen(etudid)
if decision_apo == "DEF" or decision["code"] == DEM or decision["code"] == DEF:
2020-09-26 16:19:37 +02:00
note_str = "0,01" # note non nulle pour les démissionnaires
else:
note_str = self.fmt_note(note)
2020-09-26 16:19:37 +02:00
return dict(N=note_str, B=20, J="", R=decision_apo, M="")
2023-04-19 11:51:58 +02:00
def comp_elt_annuel(self, etudid):
2020-09-26 16:19:37 +02:00
"""Calcul resultat annuel (VET) à partir du semestre courant
et de l'autre (le suivant ou le précédent complétant l'année scolaire)
En BUT, c'est la décision de jury annuelle (ApcValidationAnnee).
2020-09-26 16:19:37 +02:00
"""
# Code annuel:
# - Note: moyenne des moyennes générales des deux semestres
# (pas vraiment de sens, mais faute de mieux)
# on pourrait aussi bien prendre seulement la note du dernier semestre (S2 ou S4).
2023-05-12 22:02:14 +02:00
# XXX APOBUT: à modifier pour prendre moyenne indicative annuelle ? non
#
2020-09-26 16:19:37 +02:00
# - Résultat jury:
# si l'autre est validé, code du semestre courant (ex: S1 (ADM), S2 (AJ) => année AJ)
# si l'autre n'est pas validé ou est DEF ou DEM, code de l'autre
#
# 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']))
2023-04-19 11:51:58 +02:00
if not self.cur_sem:
2020-09-26 16:19:37 +02:00
# l'étudiant n'a pas de semestre courant ?!
self.log.append("pas de semestre courant")
log(f"comp_elt_annuel: etudid {etudid} has no cur_sem")
2020-09-26 16:19:37 +02:00
return VOID_APO_RES
2023-04-17 12:10:17 +02:00
if self.is_apc:
cur_decision = {} # comp_elt_semestre sera vide.
else:
2023-05-12 22:02:14 +02:00
# Non BUT
2023-04-19 11:51:58 +02:00
cur_decision = self.cur_res.get_etud_decision_sem(etudid)
if not cur_decision:
# pas de decision => pas de résultat annuel
return VOID_APO_RES
if (cur_decision["code"] == RAT) and not self.export_res_rat:
# ne touche pas aux RATs
return VOID_APO_RES
2020-09-26 16:19:37 +02:00
2023-04-19 11:51:58 +02:00
if not self.autre_sem:
2020-09-26 16:19:37 +02:00
# formations monosemestre, ou code VET semestriel,
# ou jury intermediaire et etudiant non redoublant...
2023-04-19 11:51:58 +02:00
return self.comp_elt_semestre(self.cur_res, cur_decision, etudid)
2020-09-26 16:19:37 +02:00
# --- Traite le BUT à part:
if self.is_apc:
2023-04-19 11:51:58 +02:00
return self.comp_elt_annuel_apc()
# --- Formations classiques
decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"])
2023-04-19 11:51:58 +02:00
autre_decision = self.autre_res.get_etud_decision_sem(etudid)
2020-09-26 16:19:37 +02:00
if not autre_decision:
# pas de decision dans l'autre => pas de résultat annuel
return VOID_APO_RES
autre_decision_apo = ScoDocSiteConfig.get_code_apo(autre_decision["code"])
2020-09-26 16:19:37 +02:00
if (
autre_decision_apo == "DEF"
or autre_decision["code"] == DEM
2020-09-26 16:19:37 +02:00
or autre_decision["code"] == DEF
) or (
decision_apo == "DEF"
or cur_decision["code"] == DEM
2020-09-26 16:19:37 +02:00
or cur_decision["code"] == DEF
):
note_str = "0,01" # note non nulle pour les démissionnaires
else:
2023-04-19 11:51:58 +02:00
note = self.cur_res.get_etud_moy_gen(etudid)
autre_note = self.autre_res.get_etud_moy_gen(etudid)
2020-09-26 16:19:37 +02:00
# print 'note=%s autre_note=%s' % (note, autre_note)
try:
moy_annuelle = (note + autre_note) / 2
2021-07-09 19:50:40 +02:00
except TypeError:
2020-09-26 16:19:37 +02:00
moy_annuelle = ""
note_str = self.fmt_note(moy_annuelle)
2020-09-26 16:19:37 +02:00
if code_semestre_validant(autre_decision["code"]):
decision_apo_annuelle = decision_apo
else:
decision_apo_annuelle = autre_decision_apo
return dict(N=note_str, B=20, J="", R=decision_apo_annuelle, M="")
2023-04-19 11:51:58 +02:00
def comp_elt_annuel_apc(self):
"""L'élément Apo pour un résultat annuel BUT.
2023-04-19 11:51:58 +02:00
self.cur_res == résultats du semestre sur lequel a été appelé l'export.
"""
2023-04-19 11:51:58 +02:00
if not self.validation_annee_but:
# pas de décision ou pas de sem. impair
return VOID_APO_RES
return dict(
2023-05-15 17:20:38 +02:00
N="", # n'exporte pas de moyenne indicative annuelle, car pas de définition officielle
2023-04-19 11:51:58 +02:00
B=20,
J="",
R=ScoDocSiteConfig.get_code_apo(self.validation_annee_but.code),
M="",
)
def _but_load_validation_annuelle(self):
"""charge la validation de jury BUT annuelle.
Ici impose qu'elle soit issue d'un semestre de l'année en cours
(pas forcément nécessaire, voir selon les retours des équipes ?)
"""
# le semestre impair de l'année scolaire
2023-04-19 11:51:58 +02:00
if self.cur_res.formsemestre.semestre_id % 2:
formsemestre = self.cur_res.formsemestre
elif (
2023-04-19 11:51:58 +02:00
self.autre_res
and self.autre_res.formsemestre.annee_scolaire()
== self.cur_res.formsemestre.annee_scolaire()
):
2023-04-19 11:51:58 +02:00
formsemestre = self.autre_res.formsemestre
assert formsemestre.semestre_id % 2
else:
# ne trouve pas de semestre impair
2023-04-19 11:51:58 +02:00
self.validation_annee_but = None
return
self.validation_annee_but: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"],
formation_id=self.cur_sem[
"formation_id"
], # XXX utiliser formation_code
2023-04-19 11:51:58 +02:00
).first()
)
2023-05-12 22:02:14 +02:00
self.is_nar = (
self.validation_annee_but and self.validation_annee_but.code == NAR
)
def etud_set_semestres_de_etape(self, apo_data: "ApoData"):
2023-04-19 11:51:58 +02:00
"""Set .cur_sem and .autre_sem et charge les résultats.
2020-09-26 16:19:37 +02:00
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
le code annuel (VET ou VRT1A (voir elt_annee_apo)).
Pour les jurys intermediaires (janvier, S1 ou S3): (S2 ou S4) de la même
étape lors d'une année précédente ?
2020-09-26 16:19:37 +02:00
2023-04-19 11:51:58 +02:00
Set cur_sem: le semestre "courant" et autre_sem, ou None s'il n'y en a pas.
2020-09-26 16:19:37 +02:00
"""
# Cherche le semestre "courant":
cur_sems = [
sem
for sem in self.etud["sems"]
if (
(sem["semestre_id"] == apo_data.cur_semestre_id)
and (apo_data.etape in sem["etapes"])
and (
sco_formsemestre.sem_in_semestre_scolaire(
sem,
apo_data.annee_scolaire,
0, # annee complete
)
2020-09-26 16:19:37 +02:00
)
)
]
if not cur_sems:
cur_sem = None
else:
# prend le plus recent avec decision
cur_sem = None
for sem in cur_sems:
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
2023-04-19 11:51:58 +02:00
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
has_decision = res.etud_has_decision(self.etud["etudid"])
if has_decision:
2020-09-26 16:19:37 +02:00
cur_sem = sem
2023-04-19 11:51:58 +02:00
self.cur_res = res
2020-09-26 16:19:37 +02:00
break
if cur_sem is None:
cur_sem = cur_sems[0] # aucun avec décision, prend le plus recent
2023-04-19 11:51:58 +02:00
if res.formsemestre.id == cur_sem["formsemestre_id"]:
self.cur_res = res
else:
formsemestre = FormSemestre.query.get_or_404(
cur_sem["formsemestre_id"]
)
self.cur_res = res_sem.load_formsemestre_results(formsemestre)
self.cur_sem = cur_sem
2020-09-26 16:19:37 +02:00
if apo_data.cur_semestre_id <= 0:
2023-04-19 11:51:58 +02:00
# "autre_sem" non pertinent pour sessions sans semestres:
self.autre_sem = None
self.autre_res = None
return
2020-09-26 16:19:37 +02:00
if apo_data.jury_intermediaire: # jury de janvier
# Le semestre suivant: exemple 2 si on est en jury de S1
autre_semestre_id = apo_data.cur_semestre_id + 1
else:
# Le précédent (S1 si on est en S2)
autre_semestre_id = apo_data.cur_semestre_id - 1
# L'autre semestre DOIT être antérieur au courant indiqué par apo_data
if apo_data.periode is not None:
if apo_data.periode == 1:
courant_annee_debut = apo_data.annee_scolaire
courant_mois_debut = 9 # periode = 1 (sept-jan)
elif apo_data.periode == 2:
courant_annee_debut = apo_data.annee_scolaire + 1
courant_mois_debut = 1 # ou 2 (fev-jul)
else:
2023-04-19 11:51:58 +02:00
raise ValueError("invalid periode value !") # bug ?
2020-09-26 16:19:37 +02:00
courant_date_debut = "%d-%02d-01" % (
courant_annee_debut,
courant_mois_debut,
)
else:
courant_date_debut = "9999-99-99"
# 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
# s'il y en a plusieurs, choisit le plus récent ayant une décision
autres_sems = []
for sem in self.etud["sems"]:
if (
sem["semestre_id"] == autre_semestre_id
and apo_data.etape_apogee in sem["etapes"]
):
if (
sem["date_debut_iso"] < courant_date_debut
): # on demande juste qu'il ait démarré avant
autres_sems.append(sem)
if not autres_sems:
autre_sem = None
elif len(autres_sems) == 1:
autre_sem = autres_sems[0]
else:
autre_sem = None
for sem in autres_sems:
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
2023-04-19 11:51:58 +02:00
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if res.is_apc:
has_decision = res.etud_has_decision(self.etud["etudid"])
else:
has_decision = res.get_etud_decision_sem(self.etud["etudid"])
if has_decision:
2020-09-26 16:19:37 +02:00
autre_sem = sem
break
if autre_sem is None:
autre_sem = autres_sems[0] # aucun avec decision, prend le plus recent
2023-04-19 11:51:58 +02:00
self.autre_sem = autre_sem
# Charge les résultats:
if autre_sem:
formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"])
self.autre_res = res_sem.load_formsemestre_results(formsemestre)
else:
self.autre_res = None
2020-09-26 16:19:37 +02:00
class ApoData:
2020-09-26 16:19:37 +02:00
def __init__(
self,
data: str,
2020-09-26 16:19:37 +02:00
periode=None,
export_res_etape=True,
export_res_sem=True,
export_res_ues=True,
export_res_modules=True,
export_res_sdj=True,
export_res_rat=True,
orig_filename=None,
):
"""Lecture du fichier CSV Apogée
Regroupe les élements importants d'un fichier CSV Apogée
periode = 1 (sept-jan) ou 2 (fev-jul), mais cette info n'est pas
(toujours) présente dans les CSV Apogée et doit être indiquée par l'utilisateur
Laisser periode à None si etape en 1 semestre (LP, décalés, ...)
"""
self.export_res_etape = export_res_etape # VET, ...
self.export_res_sem = export_res_sem # elt_sem_apo
self.export_res_ues = export_res_ues
self.export_res_modules = export_res_modules
self.export_res_sdj = export_res_sdj
self.export_res_rat = export_res_rat
self.orig_filename = orig_filename
self.periode = periode #
"1 sem. sept-jan, 2 sem. fev-jul. 0 si étape en 1 seul semestre."
self.is_apc = None
"Vrai si BUT"
2020-09-26 16:19:37 +02:00
try:
self.apo_csv = ApoCSVReadWrite(data)
2021-12-03 14:13:49 +01:00
except ScoFormatError as e:
# enrichit le message d'erreur
filename = self.orig_filename or e.filename
2021-12-03 14:13:49 +01:00
raise ScoFormatError(
f"""<h3>Erreur lecture du fichier Apogée <tt>{filename}</tt></h3>
<p>{e.args[0]}</p>"""
) from e
2020-09-26 16:19:37 +02:00
self.etape_apogee = self.get_etape_apogee() # 'V1RT'
self.vdi_apogee = self.get_vdi_apogee() # '111'
self.etape = ApoEtapeVDI(etape=self.etape_apogee, vdi=self.vdi_apogee)
self.cod_dip_apogee = self.get_cod_dip_apogee()
self.annee_scolaire = self.get_annee_scolaire()
self.jury_intermediaire = (
False # True si jury à mi-étape, eg jury de S1 dans l'étape (S1, S2)
)
# Crée les étudiants
self.etuds = [
ApoEtud(
apo_etud_tuple,
export_res_etape=export_res_etape,
export_res_sem=export_res_sem,
export_res_ues=export_res_ues,
export_res_modules=export_res_modules,
export_res_sdj=export_res_sdj,
export_res_rat=export_res_rat,
)
for apo_etud_tuple in self.apo_csv.csv_etuds
]
self.etud_by_nip = {apo_etud["nip"]: apo_etud for apo_etud in self.etuds}
log(f"ApoData( periode={self.periode}, annee_scolaire={self.annee_scolaire} )")
2020-09-26 16:19:37 +02:00
def setup(self):
2021-01-01 18:40:47 +01:00
"""Recherche semestres ScoDoc concernés"""
self.sems_etape = comp_apo_sems(self.etape_apogee, self.annee_scolaire)
if not self.sems_etape:
raise ScoValueError("aucun semestre trouvé !")
self.formsemestres_etape = [
FormSemestre.query.get_or_404(s["formsemestre_id"]) for s in self.sems_etape
]
apcs = {
formsemestre.formation.is_apc() for formsemestre in self.formsemestres_etape
}
if len(apcs) != 1:
raise ScoValueError(
"l'ensemble mixe des semestres BUT (APC) et des semestres classiques !"
)
self.is_apc = apcs.pop()
2020-09-26 16:19:37 +02:00
self.etape_formsemestre_ids = {s["formsemestre_id"] for s in self.sems_etape}
if self.periode is not None:
2020-09-26 16:19:37 +02:00
self.sems_periode = [
s
for s in self.sems_etape
if (s["periode"] == self.periode) or s["semestre_id"] < 0
]
if not self.sems_periode:
log("** Warning: ApoData.setup: sems_periode is empty")
log(
"** (periode=%s, sems_etape [periode]=%s)"
% (self.periode, [s["periode"] for s in self.sems_etape])
)
self.sems_periode = None
self.cur_semestre_id = -1 # ?
else:
self.cur_semestre_id = self.sems_periode[0]["semestre_id"]
# Les semestres de la période ont le même indice, n'est-ce pas ?
if not all(
self.cur_semestre_id == s["semestre_id"] for s in self.sems_periode
):
# debugging information
log(
2022-09-15 14:13:25 +02:00
f"""*** ApoData.set() error !
ApoData( periode={self.periode}, annee_scolaire={self.annee_scolaire
}, cur_semestre_id={self.cur_semestre_id} )
{len(self.sems_periode)} semestres dans la periode:
"""
2020-09-26 16:19:37 +02:00
)
for s in self.sems_periode:
log(pprint.pformat(s))
2022-09-15 14:13:25 +02:00
raise ScoValueError(
f"""Incohérence détectée !
Les semestres de la période n'ont pas tous le même indice.
Période: {self.periode}. Indice courant: {self.cur_semestre_id}
(au besoin, contacter l'assistance sur {scu.SCO_DISCORD_ASSISTANCE})
"""
2020-09-26 16:19:37 +02:00
)
# Cette condition sera inadaptée si semestres décalés
# (mais ils n'ont pas d'étape annuelle, espérons!)
if self.cur_semestre_id >= 0: # non pertinent pour sessions sans semestres
self.jury_intermediaire = (self.cur_semestre_id % 2) != 0
else:
self.sems_periode = None
def get_etape_apogee(self) -> str:
2021-01-01 18:40:47 +01:00
"""Le code etape: 'V1RT', donné par le code de l'élément VET"""
for elt in self.apo_csv.apo_elts.values():
2020-09-26 16:19:37 +02:00
if elt.type_objet == "VET":
return elt.code
raise ScoValueError("Pas de code etape Apogee (manque élément VET)")
def get_vdi_apogee(self) -> str:
2020-09-26 16:19:37 +02:00
"""le VDI (version de diplôme), stocké dans l'élément VET
(note: on pourrait peut-être aussi bien le récupérer dans
l'en-tête XX-APO_TITRES-XX apoC_cod_vdi)
2020-09-26 16:19:37 +02:00
"""
for elt in self.apo_csv.apo_elts.values():
2020-09-26 16:19:37 +02:00
if elt.type_objet == "VET":
return elt.version
raise ScoValueError("Pas de VDI Apogee (manque élément VET)")
def get_cod_dip_apogee(self) -> str:
2020-09-26 16:19:37 +02:00
"""Le code diplôme, indiqué dans l'en-tête de la maquette
exemple: VDTRT
Retourne '' si absent.
"""
return self.apo_csv.titles.get("apoC_cod_dip", "")
2020-09-26 16:19:37 +02:00
def get_annee_scolaire(self) -> int:
2020-09-26 16:19:37 +02:00
"""Annee scolaire du fichier Apogee: un integer
= annee du mois de septembre de début
"""
m = re.match("[12][0-9]{3}", self.apo_csv.titles["apoC_annee"])
2020-09-26 16:19:37 +02:00
if not m:
2021-12-03 14:13:49 +01:00
raise ScoFormatError(
f"""Annee scolaire (apoC_annee) invalide: "{self.apo_csv.titles["apoC_annee"]}" """
2020-09-26 16:19:37 +02:00
)
return int(m.group(0))
def list_unknown_elements(self) -> list[str]:
2020-09-26 16:19:37 +02:00
"""Liste des codes des elements Apogee non trouvés dans ScoDoc
(après traitement de tous les étudiants)
"""
codes = set()
for apo_etud in self.etuds:
codes.update(
{code for code in apo_etud.col_elts if apo_etud.col_elts[code] is None}
)
codes_list = list(codes)
codes_list.sort()
return codes_list
2020-09-26 16:19:37 +02:00
def list_elements(self) -> tuple[set[str], set[str]]:
2020-09-26 16:19:37 +02:00
"""Liste les codes des elements Apogée de la maquette
et ceux des semestres ScoDoc associés
Retourne deux ensembles
"""
try:
maq_elems = {
self.apo_csv.cols[col_id]["Code"] for col_id in self.apo_csv.col_ids[4:]
}
except KeyError as exc:
2020-09-26 16:19:37 +02:00
# une colonne déclarée dans l'en-tête n'est pas présente
declared = self.apo_csv.col_ids[4:] # id des colones dans l'en-tête
present = sorted(self.apo_csv.cols.keys()) # colonnes présentes
2020-09-26 16:19:37 +02:00
log("Fichier Apogee invalide:")
log(f"Colonnes declarees: {declared}")
log(f"Colonnes presentes: {present}")
2021-12-03 14:13:49 +01:00
raise ScoFormatError(
f"""Fichier Apogee invalide<br>Colonnes declarees: <tt>{declared}</tt>
<br>Colonnes presentes: <tt>{present}</tt>"""
) from exc
2020-09-26 16:19:37 +02:00
# l'ensemble de tous les codes des elements apo des semestres:
2021-07-09 17:47:06 +02:00
sem_elems = reduce(set.union, list(self.get_codes_by_sem().values()), set())
2020-09-26 16:19:37 +02:00
return maq_elems, sem_elems
def get_codes_by_sem(self) -> dict[int, set[str]]:
"""Pour chaque semestre associé, donne l'ensemble des codes de cette maquette Apogée
qui s'y trouvent (dans le semestre, les UE ou les modules).
Return: { formsemestre_id : { 'code1', 'code2', ... }}
2020-09-26 16:19:37 +02:00
"""
codes_by_sem = {}
for sem in self.sems_etape:
2022-07-02 11:17:04 +02:00
formsemestre: FormSemestre = FormSemestre.query.get_or_404(
sem["formsemestre_id"]
)
# L'ensemble des codes apo associés aux éléments:
codes_semestre = formsemestre.get_codes_apogee()
2022-07-02 00:00:29 +02:00
codes_modules = set().union(
*[
modimpl.module.get_codes_apogee()
for modimpl in formsemestre.modimpls
]
)
codes_ues = set().union(
*[ue.get_codes_apogee() for ue in formsemestre.get_ues(with_sport=True)]
2022-07-02 00:00:29 +02:00
)
2020-09-26 16:19:37 +02:00
s = set()
codes_by_sem[sem["formsemestre_id"]] = s
for col_id in self.apo_csv.col_ids[4:]:
code = self.apo_csv.cols[col_id]["Code"] # 'V1RT'
2022-07-02 11:17:04 +02:00
# associé à l'étape, l'année ou le semestre:
if code in codes_semestre:
2020-09-26 16:19:37 +02:00
s.add(code)
continue
# associé à une UE:
2022-07-02 00:00:29 +02:00
if code in codes_ues:
s.add(code)
continue
2020-09-26 16:19:37 +02:00
# associé à un module:
2022-07-02 00:00:29 +02:00
if code in codes_modules:
s.add(code)
2020-09-26 16:19:37 +02:00
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
return codes_by_sem
def build_cr_table(self):
2021-01-01 18:40:47 +01:00
"""Table compte rendu des décisions"""
rows = [] # tableau compte rendu des decisions
for apo_etud in self.etuds:
2020-09-26 16:19:37 +02:00
cr = {
"NIP": apo_etud["nip"],
"nom": apo_etud["nom"],
"prenom": apo_etud["prenom"],
"est_NAR": apo_etud.is_nar,
"commentaire": "; ".join(apo_etud.log),
2020-09-26 16:19:37 +02:00
}
if apo_etud.col_elts and apo_etud.col_elts[self.etape_apogee] is not None:
cr["etape"] = apo_etud.col_elts[self.etape_apogee].get("R", "")
cr["etape_note"] = apo_etud.col_elts[self.etape_apogee].get("N", "")
2020-09-26 16:19:37 +02:00
else:
cr["etape"] = ""
cr["etape_note"] = ""
rows.append(cr)
2020-09-26 16:19:37 +02:00
columns_ids = ["NIP", "nom", "prenom"]
columns_ids.extend(("etape", "etape_note", "est_NAR", "commentaire"))
T = GenTable(
columns_ids=columns_ids,
titles=dict(zip(columns_ids, columns_ids)),
rows=rows,
2020-09-26 16:19:37 +02:00
xls_sheet_name="Decisions ScoDoc",
)
return T
def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
2020-09-26 16:19:37 +02:00
"""
:param etape_apogee: etape (string or ApoEtapeVDI)
:param annee_scolaire: annee (int)
:return: list of sems for etape_apogee in annee_scolaire
"""
return sco_formsemestre.list_formsemestre_by_etape(
2021-08-19 10:28:35 +02:00
etape_apo=str(etape_apogee), annee_scolaire=annee_scolaire
2020-09-26 16:19:37 +02:00
)
def nar_etuds_table(apo_data, nar_etuds):
2021-01-01 18:40:47 +01:00
"""Liste les NAR -> excel table"""
2020-09-26 16:19:37 +02:00
code_etape = apo_data.etape_apogee
today = datetime.datetime.today().strftime("%d/%m/%y")
rows = []
nar_etuds.sort(key=lambda k: k["nom"])
for e in nar_etuds:
rows.append(
2020-09-26 16:19:37 +02:00
{
"nom": e["nom"],
"prenom": e["prenom"],
"c0": "",
"c1": "AD",
"etape": code_etape,
"c3": "",
"c4": "",
"c5": "",
"c6": "N",
"c7": "",
"c8": "",
"NIP": e["nip"],
"c10": "",
"c11": "",
"c12": "",
"c13": "NAR - Jury",
"date": today,
}
)
columns_ids = (
"NIP",
"nom",
"prenom",
"etape",
"c0",
"c1",
"c3",
"c4",
"c5",
"c6",
"c7",
"c8",
"c10",
"c11",
"c12",
"c13",
"date",
)
table = GenTable(
2020-09-26 16:19:37 +02:00
columns_ids=columns_ids,
titles=dict(zip(columns_ids, columns_ids)),
rows=rows,
2020-09-26 16:19:37 +02:00
xls_sheet_name="NAR ScoDoc",
)
return table.excel()
2020-09-26 16:19:37 +02:00
def export_csv_to_apogee(
apo_csv_data: str,
2020-09-26 16:19:37 +02:00
periode=None,
dest_zip=None,
export_res_etape=True,
export_res_sem=True,
export_res_ues=True,
export_res_modules=True,
export_res_sdj=True,
export_res_rat=True,
):
2021-01-01 18:40:47 +01:00
"""Genere un fichier CSV Apogée
2020-09-26 16:19:37 +02:00
à partir d'un fichier CSV Apogée vide (ou partiellement rempli)
et des résultats ScoDoc.
Si dest_zip, ajoute les fichiers générés à ce zip
sinon crée un zip et le publie
"""
apo_data = ApoData(
apo_csv_data,
periode=periode,
export_res_etape=export_res_etape,
export_res_sem=export_res_sem,
export_res_ues=export_res_ues,
export_res_modules=export_res_modules,
export_res_sdj=export_res_sdj,
export_res_rat=export_res_rat,
)
apo_data.setup() # -> .sems_etape
apo_csv = apo_data.apo_csv
for apo_etud in apo_data.etuds:
apo_etud.is_apc = apo_data.is_apc
apo_etud.lookup_scodoc(apo_data.etape_formsemestre_ids)
apo_etud.associate_sco(apo_data)
2020-09-26 16:19:37 +02:00
# Ré-écrit le fichier Apogée
csv_data = apo_csv.write(apo_data.etuds)
2020-09-26 16:19:37 +02:00
# Table des NAR:
nar_etuds = [apo_etud for apo_etud in apo_data.etuds if apo_etud.is_nar]
if nar_etuds:
nar_xls = nar_etuds_table(apo_data, nar_etuds)
2020-09-26 16:19:37 +02:00
else:
nar_xls = None
# Journaux & Comptes-rendus
# Orphelins: etudiants dans fichier Apogée mais pas dans ScoDoc
apo_non_scodoc = [
apo_etud for apo_etud in apo_data.etuds if apo_etud.etat == ETUD_ORPHELIN
]
2020-09-26 16:19:37 +02:00
# Non inscrits: connus de ScoDoc mais pas inscrit dans l'étape cette année
apo_non_scodoc_inscrits = [
apo_etud for apo_etud in apo_data.etuds if apo_etud.etat == ETUD_NON_INSCRIT
]
2020-09-26 16:19:37 +02:00
# CR table
cr_table = apo_data.build_cr_table()
cr_xls = cr_table.excel()
# Create ZIP
if not dest_zip:
data = io.BytesIO()
2020-09-26 16:19:37 +02:00
dest_zip = ZipFile(data, "w")
my_zip = True
else:
my_zip = False
# Ensure unique filenames
filename = apo_csv.get_filename()
2020-09-26 16:19:37 +02:00
basename, ext = os.path.splitext(filename)
csv_filename = filename
if csv_filename in dest_zip.namelist():
basename = filename + "-" + apo_data.vdi_apogee
csv_filename = basename + ext
num_file = 1
2020-09-26 16:19:37 +02:00
tmplname = basename
while csv_filename in dest_zip.namelist():
basename = f"{tmplname}-{num_file}"
2020-09-26 16:19:37 +02:00
csv_filename = basename + ext
num_file += 1
2020-09-26 16:19:37 +02:00
log_filename = "scodoc-" + basename + ".log.txt"
2021-08-12 14:49:53 +02:00
nar_filename = basename + "-nar" + scu.XLSX_SUFFIX
cr_filename = basename + "-decisions" + scu.XLSX_SUFFIX
2020-09-26 16:19:37 +02:00
logf = io.StringIO()
logf.write(f"export_to_apogee du {time.ctime()}\n\n")
2020-09-26 16:19:37 +02:00
logf.write("Semestres ScoDoc sources:\n")
for sem in apo_data.sems_etape:
logf.write("\t%(titremois)s\n" % sem)
def vrai(val):
return "vrai" if int(val) else "faux"
logf.write(f"Période: {periode}\n")
logf.write(f"exporte résultat à l'étape: {vrai(export_res_etape)}\n")
logf.write(f"exporte résultat à l'année: {vrai(export_res_sem)}\n")
logf.write(f"exporte résultats des UEs: {vrai(export_res_ues)}\n")
logf.write(f"exporte résultats des modules: {vrai(export_res_modules)}\n")
logf.write(f"exporte résultats sans décision de jury: {vrai(export_res_sdj)}\n")
2020-09-26 16:19:37 +02:00
logf.write(
"\nÉtudiants Apogée non trouvés dans ScoDoc:\n"
2020-09-26 16:19:37 +02:00
+ "\n".join(
["%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"]) for e in apo_non_scodoc]
2020-09-26 16:19:37 +02:00
)
)
logf.write(
"\nÉtudiants Apogée non inscrits sur ScoDoc dans cette étape:\n"
2020-09-26 16:19:37 +02:00
+ "\n".join(
[
"%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"])
for e in apo_non_scodoc_inscrits
2020-09-26 16:19:37 +02:00
]
)
)
logf.write(
"\n\nElements Apogee inconnus dans ces semestres ScoDoc:\n"
+ "\n".join(apo_data.list_unknown_elements())
)
log(logf.getvalue()) # sortie aussi sur le log ScoDoc
# Write data to ZIP
dest_zip.writestr(csv_filename, csv_data)
dest_zip.writestr(log_filename, logf.getvalue())
if nar_xls:
dest_zip.writestr(nar_filename, nar_xls)
dest_zip.writestr(cr_filename, cr_xls)
if my_zip:
dest_zip.close()
data.seek(0)
return send_file(
data,
mimetype="application/zip",
2021-08-31 20:18:50 +02:00
download_name=scu.sanitize_filename(basename + "-scodoc.zip"),
as_attachment=True,
2020-09-26 16:19:37 +02:00
)
else:
return None # zip modified in place