Fichiers Apogée: code refactoring + test unitaire

This commit is contained in:
Emmanuel Viennet 2023-05-11 14:01:23 +02:00
parent 8f844f5191
commit daa5b8ac53
12 changed files with 951 additions and 634 deletions

View File

@ -195,11 +195,14 @@ class FormSemestre(db.Model):
d["date_fin"] = d["date_fin_iso"] = "" d["date_fin"] = d["date_fin_iso"] = ""
d["responsables"] = [u.id for u in self.responsables] d["responsables"] = [u.id for u in self.responsables]
d["titre_formation"] = self.titre_formation() d["titre_formation"] = self.titre_formation()
if convert_objects: if convert_objects: # pour API
d["parcours"] = [p.to_dict() for p in self.get_parcours_apc()] d["parcours"] = [p.to_dict() for p in self.get_parcours_apc()]
d["departement"] = self.departement.to_dict() d["departement"] = self.departement.to_dict()
d["formation"] = self.formation.to_dict() d["formation"] = self.formation.to_dict()
d["etape_apo"] = self.etapes_apo_str() d["etape_apo"] = self.etapes_apo_str()
else:
# Converti les étapes Apogee sous forme d'ApoEtapeVDI (compat scodoc7)
d["etapes"] = [e.as_apovdi() for e in self.etapes]
return d return d
def to_dict_api(self): def to_dict_api(self):
@ -937,7 +940,7 @@ class FormSemestreEtape(db.Model):
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 as_apovdi(self): def as_apovdi(self) -> ApoEtapeVDI:
return ApoEtapeVDI(self.etape_apo) return ApoEtapeVDI(self.etape_apo)

View File

@ -46,13 +46,14 @@ Pour chaque étudiant commun:
from flask import g, url_for from flask import g, url_for
from app import log from app import log
from app.scodoc import sco_apogee_csv from app.scodoc import sco_apogee_csv, sco_apogee_reader
from app.scodoc.sco_apogee_csv import ApoData
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
_help_txt = """ _HELP_TXT = """
<div class="help"> <div class="help">
<p>Outil de comparaison de fichiers (maquettes CSV) Apogée. <p>Outil de comparaison de fichiers (maquettes CSV) Apogée.
</p> </p>
@ -69,7 +70,7 @@ def apo_compare_csv_form():
"""<h2>Comparaison de fichiers Apogée</h2> """<h2>Comparaison de fichiers Apogée</h2>
<form id="apo_csv_add" action="apo_compare_csv" method="post" enctype="multipart/form-data"> <form id="apo_csv_add" action="apo_compare_csv" method="post" enctype="multipart/form-data">
""", """,
_help_txt, _HELP_TXT,
""" """
<div class="apo_compare_csv_form_but"> <div class="apo_compare_csv_form_but">
Fichier Apogée A: Fichier Apogée A:
@ -109,14 +110,14 @@ def apo_compare_csv(file_a, file_b, autodetect=True):
raise ScoValueError( raise ScoValueError(
f""" f"""
Erreur: l'encodage de l'un des fichiers est incorrect. Erreur: l'encodage de l'un des fichiers est incorrect.
Vérifiez qu'il est bien en {sco_apogee_csv.APO_INPUT_ENCODING} Vérifiez qu'il est bien en {sco_apogee_reader.APO_INPUT_ENCODING}
""", """,
dest_url=dest_url, dest_url=dest_url,
) from exc ) from exc
H = [ H = [
html_sco_header.sco_header(page_title="Comparaison de fichiers Apogée"), html_sco_header.sco_header(page_title="Comparaison de fichiers Apogée"),
"<h2>Comparaison de fichiers Apogée</h2>", "<h2>Comparaison de fichiers Apogée</h2>",
_help_txt, _HELP_TXT,
'<div class="apo_compare_csv">', '<div class="apo_compare_csv">',
_apo_compare_csv(apo_data_a, apo_data_b), _apo_compare_csv(apo_data_a, apo_data_b),
"</div>", "</div>",
@ -130,17 +131,17 @@ def _load_apo_data(csvfile, autodetect=True):
"Read data from request variable and build ApoData" "Read data from request variable and build ApoData"
data_b = csvfile.read() data_b = csvfile.read()
if autodetect: if autodetect:
data_b, message = sco_apogee_csv.fix_data_encoding(data_b) data_b, message = sco_apogee_reader.fix_data_encoding(data_b)
if message: if message:
log(f"apo_compare_csv: {message}") log(f"apo_compare_csv: {message}")
if not data_b: if not data_b:
raise ScoValueError("fichier vide ? (apo_compare_csv: no data)") raise ScoValueError("fichier vide ? (apo_compare_csv: no data)")
data = data_b.decode(sco_apogee_csv.APO_INPUT_ENCODING) data = data_b.decode(sco_apogee_reader.APO_INPUT_ENCODING)
apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename) apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
return apo_data return apo_data
def _apo_compare_csv(A, B): def _apo_compare_csv(apo_a: ApoData, apo_b: ApoData):
"""Generate html report comparing A and B, two instances of ApoData """Generate html report comparing A and B, two instances of ApoData
representing Apogee CSV maquettes. representing Apogee CSV maquettes.
""" """
@ -148,74 +149,75 @@ def _apo_compare_csv(A, B):
# 1-- Check etape and codes # 1-- Check etape and codes
L.append('<div class="section"><div class="tit">En-tête</div>') L.append('<div class="section"><div class="tit">En-tête</div>')
L.append('<div><span class="key">Nom fichier A:</span><span class="val_ok">') L.append('<div><span class="key">Nom fichier A:</span><span class="val_ok">')
L.append(A.orig_filename) L.append(apo_a.orig_filename)
L.append("</span></div>") L.append("</span></div>")
L.append('<div><span class="key">Nom fichier B:</span><span class="val_ok">') L.append('<div><span class="key">Nom fichier B:</span><span class="val_ok">')
L.append(B.orig_filename) L.append(apo_b.orig_filename)
L.append("</span></div>") L.append("</span></div>")
L.append('<div><span class="key">Étape Apogée:</span>') L.append('<div><span class="key">Étape Apogée:</span>')
if A.etape_apogee != B.etape_apogee: if apo_a.etape_apogee != apo_b.etape_apogee:
L.append( L.append(
'<span class="val_dif">%s != %s</span>' % (A.etape_apogee, B.etape_apogee) f"""<span class="val_dif">{apo_a.etape_apogee} != {apo_b.etape_apogee}</span>"""
) )
else: else:
L.append('<span class="val_ok">%s</span>' % (A.etape_apogee,)) L.append(f"""<span class="val_ok">{apo_a.etape_apogee}</span>""")
L.append("</div>") L.append("</div>")
L.append('<div><span class="key">VDI Apogée:</span>') L.append('<div><span class="key">VDI Apogée:</span>')
if A.vdi_apogee != B.vdi_apogee: if apo_a.vdi_apogee != apo_b.vdi_apogee:
L.append('<span class="val_dif">%s != %s</span>' % (A.vdi_apogee, B.vdi_apogee)) L.append(
f"""<span class="val_dif">{apo_a.vdi_apogee} != {apo_b.vdi_apogee}</span>"""
)
else: else:
L.append('<span class="val_ok">%s</span>' % (A.vdi_apogee,)) L.append(f"""<span class="val_ok">{apo_a.vdi_apogee}</span>""")
L.append("</div>") L.append("</div>")
L.append('<div><span class="key">Code diplôme :</span>') L.append('<div><span class="key">Code diplôme :</span>')
if A.cod_dip_apogee != B.cod_dip_apogee: if apo_a.cod_dip_apogee != apo_b.cod_dip_apogee:
L.append( L.append(
'<span class="val_dif">%s != %s</span>' f"""<span class="val_dif">{apo_a.cod_dip_apogee} != {apo_b.cod_dip_apogee}</span>"""
% (A.cod_dip_apogee, B.cod_dip_apogee)
) )
else: else:
L.append('<span class="val_ok">%s</span>' % (A.cod_dip_apogee,)) L.append(f"""<span class="val_ok">{apo_a.cod_dip_apogee}</span>""")
L.append("</div>") L.append("</div>")
L.append('<div><span class="key">Année scolaire :</span>') L.append('<div><span class="key">Année scolaire :</span>')
if A.annee_scolaire != B.annee_scolaire: if apo_a.annee_scolaire != apo_b.annee_scolaire:
L.append( L.append(
'<span class="val_dif">%s != %s</span>' '<span class="val_dif">%s != %s</span>'
% (A.annee_scolaire, B.annee_scolaire) % (apo_a.annee_scolaire, apo_b.annee_scolaire)
) )
else: else:
L.append('<span class="val_ok">%s</span>' % (A.annee_scolaire,)) L.append('<span class="val_ok">%s</span>' % (apo_a.annee_scolaire,))
L.append("</div>") L.append("</div>")
# Colonnes: # Colonnes:
A_elts = set(A.apo_elts.keys()) a_elts = set(apo_a.apo_csv.apo_elts.keys())
B_elts = set(B.apo_elts.keys()) b_elts = set(apo_b.apo_csv.apo_elts.keys())
L.append('<div><span class="key">Éléments Apogée :</span>') L.append('<div><span class="key">Éléments Apogée :</span>')
if A_elts == B_elts: if a_elts == b_elts:
L.append('<span class="val_ok">%d</span>' % len(A_elts)) L.append(f"""<span class="val_ok">{len(a_elts)}</span>""")
else: else:
elts_communs = A_elts.intersection(B_elts) elts_communs = a_elts.intersection(b_elts)
elts_only_A = A_elts - A_elts.intersection(B_elts) elts_only_a = a_elts - a_elts.intersection(b_elts)
elts_only_B = B_elts - A_elts.intersection(B_elts) elts_only_b = b_elts - a_elts.intersection(b_elts)
L.append( L.append(
'<span class="val_dif">différents (%d en commun, %d seulement dans A, %d seulement dans B)</span>' '<span class="val_dif">différents (%d en commun, %d seulement dans A, %d seulement dans B)</span>'
% ( % (
len(elts_communs), len(elts_communs),
len(elts_only_A), len(elts_only_a),
len(elts_only_B), len(elts_only_b),
) )
) )
if elts_only_A: if elts_only_a:
L.append( L.append(
'<div span class="key">Éléments seulement dans A : </span><span class="val_dif">%s</span></div>' '<div span class="key">Éléments seulement dans A : </span><span class="val_dif">%s</span></div>'
% ", ".join(sorted(elts_only_A)) % ", ".join(sorted(elts_only_a))
) )
if elts_only_B: if elts_only_b:
L.append( L.append(
'<div span class="key">Éléments seulement dans B : </span><span class="val_dif">%s</span></div>' '<div span class="key">Éléments seulement dans B : </span><span class="val_dif">%s</span></div>'
% ", ".join(sorted(elts_only_B)) % ", ".join(sorted(elts_only_b))
) )
L.append("</div>") L.append("</div>")
L.append("</div>") # /section L.append("</div>") # /section
@ -223,22 +225,21 @@ def _apo_compare_csv(A, B):
# 2-- # 2--
L.append('<div class="section"><div class="tit">Étudiants</div>') L.append('<div class="section"><div class="tit">Étudiants</div>')
A_nips = set(A.etud_by_nip) a_nips = set(apo_a.etud_by_nip)
B_nips = set(B.etud_by_nip) b_nips = set(apo_b.etud_by_nip)
nb_etuds_communs = len(A_nips.intersection(B_nips)) nb_etuds_communs = len(a_nips.intersection(b_nips))
nb_etuds_dif = len(A_nips.union(B_nips) - A_nips.intersection(B_nips)) nb_etuds_dif = len(a_nips.union(b_nips) - a_nips.intersection(b_nips))
L.append("""<div><span class="key">Liste d'étudiants :</span>""") L.append("""<div><span class="key">Liste d'étudiants :</span>""")
if A_nips == B_nips: if a_nips == b_nips:
L.append( L.append(
"""<span class="s_ok"> f"""<span class="s_ok">
%d étudiants (tous présents dans chaque fichier)</span> {len(a_nips)} étudiants (tous présents dans chaque fichier)</span>
""" """
% len(A_nips)
) )
else: else:
L.append( L.append(
'<span class="val_dif">différents (%d en commun, %d différents)</span>' f"""<span class="val_dif">différents ({nb_etuds_communs} en commun, {
% (nb_etuds_communs, nb_etuds_dif) nb_etuds_dif} différents)</span>"""
) )
L.append("</div>") L.append("</div>")
L.append("</div>") # /section L.append("</div>") # /section
@ -247,19 +248,22 @@ def _apo_compare_csv(A, B):
if nb_etuds_communs > 0: if nb_etuds_communs > 0:
L.append( L.append(
"""<div class="section sec_table"> """<div class="section sec_table">
<div class="tit">Différences de résultats des étudiants présents dans les deux fichiers</div> <div class="tit">Différences de résultats des étudiants présents dans les deux fichiers
</div>
<p> <p>
""" """
) )
T = apo_table_compare_etud_results(A, B) T = apo_table_compare_etud_results(apo_a, apo_b)
if T.get_nb_rows() > 0: if T.get_nb_rows() > 0:
L.append(T.html()) L.append(T.html())
else: else:
L.append( L.append(
"""<p class="p_ok">aucune différence de résultats f"""<p class="p_ok">aucune différence de résultats
sur les %d étudiants communs (<em>les éléments Apogée n'apparaissant pas dans les deux fichiers sont omis</em>)</p> sur les {nb_etuds_communs} étudiants communs
(<em>les éléments Apogée n'apparaissant pas dans les deux
fichiers sont omis</em>)
</p>
""" """
% nb_etuds_communs
) )
L.append("</div>") # /section L.append("</div>") # /section
@ -290,19 +294,17 @@ def apo_table_compare_etud_results(A, B):
def _build_etud_res(e, apo_data): def _build_etud_res(e, apo_data):
r = {} r = {}
for elt_code in apo_data.apo_elts: for elt_code in apo_data.apo_csv.apo_elts:
elt = apo_data.apo_elts[elt_code] elt = apo_data.apo_csv.apo_elts[elt_code]
try: try:
# les colonnes de cet élément # les colonnes de cet élément
col_ids_type = [ col_ids_type = [(ec["apoL_a01_code"], ec["Type Rés."]) for ec in elt.cols]
(ec["apoL_a01_code"], ec["Type R\xc3\xa9s."]) for ec in elt.cols
]
except KeyError as exc: except KeyError as exc:
raise ScoValueError( raise ScoValueError(
"Erreur: un élément sans 'Type R\xc3\xa9s.'. Vérifiez l'encodage de vos fichiers." "Erreur: un élément sans 'Type Rés.'. Vérifiez l'encodage de vos fichiers."
) from exc ) from exc
r[elt_code] = {} r[elt_code] = {}
for (col_id, type_res) in col_ids_type: for col_id, type_res in col_ids_type:
r[elt_code][type_res] = e.cols[col_id] r[elt_code][type_res] = e.cols[col_id]
return r return r

View File

@ -1,6 +1,3 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
############################################################################## ##############################################################################
# #
# Gestion scolarite IUT # Gestion scolarite IUT
@ -30,57 +27,12 @@
Ce code a été au départ inspiré par les travaux de Damien Mascré, scodoc2apogee (en Java). Ce code a été au départ inspiré par les travaux de Damien Mascré, scodoc2apogee (en Java).
A utiliser en fin de semestre, après les jury. A utiliser en fin de semestre, après les jury.
On communique avec Apogée via des fichiers CSV. On communique avec Apogée via des fichiers CSV.
Le fichier CSV, champs séparés par des tabulations, a la structure suivante: XXX A vérifier: AJAC car 1 sem. validé et pas de NAR
<pre>
XX-APO_TITRES-XX
apoC_annee 2007/2008
apoC_cod_dip VDTCJ
apoC_Cod_Exp 1
apoC_cod_vdi 111
apoC_Fichier_Exp VDTCJ_V1CJ.txt
apoC_lib_dip DUT CJ
apoC_Titre1 Export Apogée du 13/06/2008 à 14:29
apoC_Titre2
XX-APO_COLONNES-XX
apoL_a01_code Type Objet Code Version Année Session Admission/Admissibilité Type Rés. Etudiant Numéro
apoL_a02_nom 1 Nom
apoL_a03_prenom 1 Prénom
apoL_a04_naissance Session Admissibilité Naissance
APO_COL_VAL_DEB
apoL_c0001 VET V1CJ 111 2007 0 1 N V1CJ - DUT CJ an1 0 1 Note
apoL_c0002 VET V1CJ 111 2007 0 1 B 0 1 Barème
apoL_c0003 VET V1CJ 111 2007 0 1 R 0 1 Résultat
APO_COL_VAL_FIN
apoL_c0030 APO_COL_VAL_FIN
XX-APO_VALEURS-XX
apoL_a01_code apoL_a02_nom apoL_a03_prenom apoL_a04_naissance apoL_c0001 apoL_c0002 apoL_c0003 apoL_c0004 apoL_c0005 apoL_c0006 apoL_c0007 apoL_c0008 apoL_c0009 apoL_c0010 apoL_c0011 apoL_c0012 apoL_c0013 apoL_c0014 apoL_c0015 apoL_c0016 apoL_c0017 apoL_c0018 apoL_c0019 apoL_c0020 apoL_c0021 apoL_c0022 apoL_c0023 apoL_c0024 apoL_c0025 apoL_c0026 apoL_c0027 apoL_c0028 apoL_c0029
10601232 AARIF MALIKA 22/09/1986 18 20 ADM 18 20 ADM 18 20 ADM 18 20 ADM 18 20 ADM 18 20 18 20 ADM 18 20 ADM 18 20 ADM 18 20 ADM
</pre>
On récupère nos éléments pédagogiques dans la section XX-APO-COLONNES-XX et
notre liste d'étudiants dans la section XX-APO_VALEURS-XX. Les champs de la
section XX-APO_VALEURS-XX sont décrits par les lignes successives de la
section XX-APO_COLONNES-XX.
Le fichier CSV correspond à une étape, qui est récupérée sur la ligne
<pre>
apoL_c0001 VET V1CJ ...
</pre>
XXX A vérifier:
AJAC car 1 sem. validé et pas de NAR
""" """
import collections
import datetime import datetime
from functools import reduce from functools import reduce
import functools import functools
@ -94,8 +46,6 @@ from zipfile import ZipFile
from flask import send_file from flask import send_file
import numpy as np import numpy as np
# Pour la détection auto de l'encodage des fichiers Apogée:
from chardet import detect as chardet_detect
from app import log from app import log
from app.comp import res_sem from app.comp import res_sem
@ -103,6 +53,11 @@ from app.comp.res_compat import NotesTableCompat
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite, ApcValidationAnnee from app.models import FormSemestre, Identite, ApcValidationAnnee
from app.models.config import ScoDocSiteConfig 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 import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
@ -118,15 +73,6 @@ from app.scodoc import sco_cursus
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_etud from app.scodoc import sco_etud
APO_PORTAL_ENCODING = (
"utf8" # encodage du fichier CSV Apogée (était 'ISO-8859-1' avant jul. 2016)
)
APO_INPUT_ENCODING = "ISO-8859-1" #
APO_OUTPUT_ENCODING = APO_INPUT_ENCODING # encodage des fichiers Apogee générés
APO_DECIMAL_SEP = "," # separateur décimal: virgule
APO_SEP = "\t"
APO_NEWLINE = "\r\n"
def _apo_fmt_note(note, fmt="%3.2f"): def _apo_fmt_note(note, fmt="%3.2f"):
"Formatte une note pour Apogée (séparateur décimal: ',')" "Formatte une note pour Apogée (séparateur décimal: ',')"
@ -141,89 +87,6 @@ def _apo_fmt_note(note, fmt="%3.2f"):
return (fmt % val).replace(".", APO_DECIMAL_SEP) return (fmt % val).replace(".", APO_DECIMAL_SEP)
def guess_data_encoding(text, threshold=0.6):
"""Guess string encoding, using chardet heuristics.
Returns encoding, or None if detection failed (confidence below threshold)
"""
r = chardet_detect(text)
if r["confidence"] < threshold:
return None
else:
return r["encoding"]
def fix_data_encoding(
text: bytes,
default_source_encoding=APO_INPUT_ENCODING,
dest_encoding=APO_INPUT_ENCODING,
) -> tuple[bytes, str]:
"""Try to ensure that text is using dest_encoding
returns converted text, and a message describing the conversion.
Raises UnicodeEncodeError en cas de problème, en général liée à
une auto-détection errornée.
"""
message = ""
detected_encoding = guess_data_encoding(text)
if not detected_encoding:
if default_source_encoding != dest_encoding:
message = f"converting from {default_source_encoding} to {dest_encoding}"
text = text.decode(default_source_encoding).encode(dest_encoding)
else:
if detected_encoding != dest_encoding:
message = (
f"converting from detected {default_source_encoding} to {dest_encoding}"
)
text = text.decode(detected_encoding).encode(dest_encoding)
return text, message
class StringIOFileLineWrapper:
def __init__(self, data: str):
self.f = io.StringIO(data)
self.lineno = 0
def close(self):
return self.f.close()
def readline(self):
self.lineno += 1
return self.f.readline()
class DictCol(dict):
"A dict, where we can add attributes"
pass
class ApoElt(object):
"""Definition d'un Element Apogee
sur plusieurs colonnes du fichier CSV
"""
def __init__(self, cols):
assert len(cols) > 0
assert len(set([c["Code"] for c in cols])) == 1 # colonnes de meme code
assert len(set([c["Type Objet"] for c in cols])) == 1 # colonnes de meme type
self.cols = cols
self.code = cols[0]["Code"]
self.version = cols[0]["Version"]
self.type_objet = cols[0]["Type Objet"]
def append(self, col):
assert col["Code"] == self.code
if col["Type Objet"] != self.type_objet:
log(
"Warning: ApoElt: duplicate id %s (%s and %s)"
% (self.code, self.type_objet, col["Type Objet"])
)
self.type_objet = col["Type Objet"]
self.cols.append(col)
def __repr__(self):
return f"ApoElt(code='{self.code}', cols={pprint.pformat(self.cols)})"
class EtuCol: class EtuCol:
"""Valeurs colonnes d'un element pour un etudiant""" """Valeurs colonnes d'un element pour un etudiant"""
@ -243,11 +106,7 @@ class ApoEtud(dict):
def __init__( def __init__(
self, self,
nip="", apo_etud_tuple: ApoEtudTuple,
nom="",
prenom="",
naissance="",
cols={},
export_res_etape=True, export_res_etape=True,
export_res_sem=True, export_res_sem=True,
export_res_ues=True, export_res_ues=True,
@ -255,11 +114,11 @@ class ApoEtud(dict):
export_res_sdj=True, export_res_sdj=True,
export_res_rat=True, export_res_rat=True,
): ):
self["nip"] = nip self["nip"] = apo_etud_tuple.nip
self["nom"] = nom self["nom"] = apo_etud_tuple.nom
self["prenom"] = prenom self["prenom"] = apo_etud_tuple.prenom
self["naissance"] = naissance self["naissance"] = apo_etud_tuple.naissance
self.cols = cols self.cols = apo_etud_tuple.cols
"{ col_id : value } colid = 'apoL_c0001'" "{ col_id : value } colid = 'apoL_c0001'"
self.is_apc = None self.is_apc = None
"Vrai si BUT" "Vrai si BUT"
@ -268,7 +127,7 @@ class ApoEtud(dict):
self.etud: Identite = None self.etud: Identite = None
"etudiant ScoDoc associé" "etudiant ScoDoc associé"
self.etat = None # ETUD_OK, ... self.etat = None # ETUD_OK, ...
self.is_NAR = False self.is_nar = False
"True si NARé dans un semestre" "True si NARé dans un semestre"
self.log = [] self.log = []
self.has_logged_no_decision = False self.has_logged_no_decision = False
@ -344,8 +203,8 @@ class ApoEtud(dict):
) # etudiant inconnu, recopie les valeurs existantes dans Apo ) # etudiant inconnu, recopie les valeurs existantes dans Apo
else: else:
sco_elts = {} # valeurs trouvées dans ScoDoc code : { N, B, J, R } sco_elts = {} # valeurs trouvées dans ScoDoc code : { N, B, J, R }
for col_id in apo_data.col_ids[4:]: for col_id in apo_data.apo_csv.col_ids[4:]:
code = apo_data.cols[col_id]["Code"] # 'V1RT' code = apo_data.apo_csv.cols[col_id]["Code"] # 'V1RT'
elt = sco_elts.get(code, None) elt = sco_elts.get(code, None)
# elt est {'R': ADM, 'J': '', 'B': 20, 'N': '12.14'} # elt est {'R': ADM, 'J': '', 'B': 20, 'N': '12.14'}
if elt is None: # pas déjà trouvé if elt is None: # pas déjà trouvé
@ -361,7 +220,7 @@ class ApoEtud(dict):
else: else:
try: try:
self.new_cols[col_id] = sco_elts[code][ self.new_cols[col_id] = sco_elts[code][
apo_data.cols[col_id]["Type Rés."] apo_data.apo_csv.cols[col_id]["Type Rés."]
] ]
except KeyError as exc: except KeyError as exc:
log( log(
@ -374,12 +233,12 @@ class ApoEtud(dict):
(vérifier qu'il est bien associé à une UE ou semestre)?""" (vérifier qu'il est bien associé à une UE ou semestre)?"""
) from exc ) from exc
# recopie les 4 premieres colonnes (nom, ..., naissance): # recopie les 4 premieres colonnes (nom, ..., naissance):
for col_id in apo_data.col_ids[:4]: for col_id in apo_data.apo_csv.col_ids[:4]:
self.new_cols[col_id] = self.cols[col_id] self.new_cols[col_id] = self.cols[col_id]
# def unassociated_codes(self, apo_data): # def unassociated_codes(self, apo_data):
# "list of apo elements for this student without a value in ScoDoc" # "list of apo elements for this student without a value in ScoDoc"
# codes = set([apo_data.cols[col_id].code for col_id in apo_data.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, sem) -> dict:
@ -431,7 +290,7 @@ class ApoEtud(dict):
else: else:
decision = res.get_etud_decision_sem(etudid) decision = res.get_etud_decision_sem(etudid)
if decision and decision["code"] == NAR: if decision and decision["code"] == NAR:
self.is_NAR = True self.is_nar = True
# Element semestre: (non BUT donc) # Element semestre: (non BUT donc)
if code in {x.strip() for x in sem["elt_sem_apo"].split(",")}: if code in {x.strip() for x in sem["elt_sem_apo"].split(",")}:
if self.export_res_sem: if self.export_res_sem:
@ -634,7 +493,7 @@ class ApoEtud(dict):
).first() ).first()
) )
def etud_set_semestres_de_etape(self, apo_data): def etud_set_semestres_de_etape(self, apo_data: "ApoData"):
"""Set .cur_sem and .autre_sem et charge les résultats. """Set .cur_sem and .autre_sem 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
@ -789,24 +648,14 @@ class ApoData:
"1 sem. sept-jan, 2 sem. fev-jul. 0 si étape en 1 seul semestre." "1 sem. sept-jan, 2 sem. fev-jul. 0 si étape en 1 seul semestre."
self.is_apc = None self.is_apc = None
"Vrai si BUT" "Vrai si BUT"
self.header: str = ""
"début du fichier Apogée (sera ré-écrit non modifié)"
self.titles: dict[str, str] = {}
"titres Apogée (section XX-APO_TITRES-XX)"
try: try:
self.read_csv(data) self.apo_csv = ApoCSVReadWrite(data)
except ScoFormatError as e: except ScoFormatError as e:
# essaie de retrouver le nom du fichier pour enrichir le message d'erreur # enrichit le message d'erreur
filename = "" filename = self.orig_filename or e.filename
if self.orig_filename is None:
if hasattr(self, "titles"):
filename = self.titles.get("apoC_Fichier_Exp", filename)
else:
filename = self.orig_filename
raise ScoFormatError( raise ScoFormatError(
"<h3>Erreur lecture du fichier Apogée <tt>%s</tt></h3><p>" % filename f"""<h3>Erreur lecture du fichier Apogée <tt>{filename}</tt></h3>
+ e.args[0] <p>{e.args[0]}</p>"""
+ "</p>"
) from e ) from e
self.etape_apogee = self.get_etape_apogee() # 'V1RT' self.etape_apogee = self.get_etape_apogee() # 'V1RT'
self.vdi_apogee = self.get_vdi_apogee() # '111' self.vdi_apogee = self.get_vdi_apogee() # '111'
@ -816,12 +665,27 @@ class ApoData:
self.jury_intermediaire = ( self.jury_intermediaire = (
False # True si jury à mi-étape, eg jury de S1 dans l'étape (S1, S2) 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} )") log(f"ApoData( periode={self.periode}, annee_scolaire={self.annee_scolaire} )")
def setup(self): def setup(self):
"""Recherche semestres ScoDoc concernés""" """Recherche semestres ScoDoc concernés"""
self.sems_etape = comp_apo_sems(self.etape_apogee, self.annee_scolaire) 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 = [ self.formsemestres_etape = [
FormSemestre.query.get_or_404(s["formsemestre_id"]) for s in self.sems_etape FormSemestre.query.get_or_404(s["formsemestre_id"]) for s in self.sems_etape
] ]
@ -882,205 +746,74 @@ class ApoData:
else: else:
self.sems_periode = None self.sems_periode = None
def read_csv(self, data: str): def get_etape_apogee(self) -> str:
if not data:
raise ScoFormatError("Fichier Apogée vide !")
f = StringIOFileLineWrapper(data) # pour traiter comme un fichier
# check that we are at the begining of Apogee CSV
line = f.readline().strip()
if line != "XX-APO_TITRES-XX":
raise ScoFormatError("format incorrect: pas de XX-APO_TITRES-XX")
# 1-- En-tête: du début jusqu'à la balise XX-APO_VALEURS-XX
try:
idx = data.index("XX-APO_VALEURS-XX")
except ValueError as exc:
raise ScoFormatError("format incorrect: pas de XX-APO_VALEURS-XX") from exc
self.header = data[:idx]
# 2-- Titres:
# on va y chercher apoC_Fichier_Exp qui donnera le nom du fichier
# ainsi que l'année scolaire et le code diplôme.
self.titles = _apo_read_TITRES(f)
# 3-- La section XX-APO_TYP_RES-XX est ignorée:
line = f.readline().strip()
if line != "XX-APO_TYP_RES-XX":
raise ScoFormatError("format incorrect: pas de XX-APO_TYP_RES-XX")
_apo_skip_section(f)
# 4-- Définition de colonnes: (on y trouve aussi l'étape)
line = f.readline().strip()
if line != "XX-APO_COLONNES-XX":
raise ScoFormatError("format incorrect: pas de XX-APO_COLONNES-XX")
self.cols = _apo_read_cols(f)
self.apo_elts = self._group_elt_cols(self.cols)
# 5-- Section XX-APO_VALEURS-XX
# Lecture des étudiants et de leurs résultats
while True: # skip
line = f.readline()
if not line:
raise ScoFormatError("format incorrect: pas de XX-APO_VALEURS-XX")
if line.strip() == "XX-APO_VALEURS-XX":
break
self.column_titles = f.readline()
self.col_ids = self.column_titles.strip().split()
self.etuds = self.apo_read_etuds(f)
self.etud_by_nip = {e["nip"]: e for e in self.etuds}
def get_etud_by_nip(self, nip):
"returns ApoEtud with a given NIP code"
return self.etud_by_nip[nip]
def _group_elt_cols(self, cols):
"""Return ordered dict of ApoElt from list of ApoCols.
Clé: id apogée, eg 'V1RT', 'V1GE2201', ...
Valeur: ApoElt, avec les attributs code, type_objet
Si les id Apogée ne sont pas uniques (ce n'est pas garanti), garde le premier
"""
elts = collections.OrderedDict()
for col_id in sorted(list(cols.keys()), reverse=True):
col = cols[col_id]
if col["Code"] in elts:
elts[col["Code"]].append(col)
else:
elts[col["Code"]] = ApoElt([col])
return elts # { code apo : ApoElt }
def apo_read_etuds(self, f) -> list[ApoEtud]:
"""Lecture des étudiants (et résultats) du fichier CSV Apogée.
Les lignes "étudiant" commencent toujours par
`12345678 NOM PRENOM 15/05/2003`
le premier code étant le NIP.
"""
apo_etuds = []
while True:
line = f.readline()
if not line:
break
if not line.strip():
continue # silently ignore blank lines
line = line.strip(APO_NEWLINE)
fields = line.split(APO_SEP)
if len(fields) < 4:
raise ScoValueError(
"""Ligne étudiant invalide
(doit commencer par 'NIP NOM PRENOM dd/mm/yyyy')"""
)
cols = {} # { col_id : value }
for i, field in enumerate(fields):
cols[self.col_ids[i]] = field
apo_etuds.append(
ApoEtud(
nip=fields[0], # id etudiant
nom=fields[1],
prenom=fields[2],
naissance=fields[3],
cols=cols,
export_res_etape=self.export_res_etape,
export_res_sem=self.export_res_sem,
export_res_ues=self.export_res_ues,
export_res_modules=self.export_res_modules,
export_res_sdj=self.export_res_sdj,
export_res_rat=self.export_res_rat,
)
)
return apo_etuds
def get_etape_apogee(self):
"""Le code etape: 'V1RT', donné par le code de l'élément VET""" """Le code etape: 'V1RT', donné par le code de l'élément VET"""
for elt in self.apo_elts.values(): for elt in self.apo_csv.apo_elts.values():
if elt.type_objet == "VET": if elt.type_objet == "VET":
return elt.code return elt.code
raise ScoValueError("Pas de code etape Apogee (manque élément VET)") raise ScoValueError("Pas de code etape Apogee (manque élément VET)")
def get_vdi_apogee(self): def get_vdi_apogee(self) -> str:
"""le VDI (version de diplôme), stocké dans l'élément VET """le VDI (version de diplôme), stocké dans l'élément VET
(note: on pourrait peut-être aussi bien le récupérer dans (note: on pourrait peut-être aussi bien le récupérer dans
l'en-tête XX-APO_TITRES-XX apoC_cod_vdi) l'en-tête XX-APO_TITRES-XX apoC_cod_vdi)
""" """
for elt in self.apo_elts.values(): for elt in self.apo_csv.apo_elts.values():
if elt.type_objet == "VET": if elt.type_objet == "VET":
return elt.version return elt.version
raise ScoValueError("Pas de VDI Apogee (manque élément VET)") raise ScoValueError("Pas de VDI Apogee (manque élément VET)")
def get_cod_dip_apogee(self): def get_cod_dip_apogee(self) -> str:
"""Le code diplôme, indiqué dans l'en-tête de la maquette """Le code diplôme, indiqué dans l'en-tête de la maquette
exemple: VDTRT exemple: VDTRT
Retourne '' si absent. Retourne '' si absent.
""" """
return self.titles.get("apoC_cod_dip", "") return self.apo_csv.titles.get("apoC_cod_dip", "")
def get_annee_scolaire(self): def get_annee_scolaire(self) -> int:
"""Annee scolaire du fichier Apogee: un integer """Annee scolaire du fichier Apogee: un integer
= annee du mois de septembre de début = annee du mois de septembre de début
""" """
m = re.match("[12][0-9]{3}", self.titles["apoC_annee"]) m = re.match("[12][0-9]{3}", self.apo_csv.titles["apoC_annee"])
if not m: if not m:
raise ScoFormatError( raise ScoFormatError(
f"""Annee scolaire (apoC_annee) invalide: "{self.titles["apoC_annee"]}" """ f"""Annee scolaire (apoC_annee) invalide: "{self.apo_csv.titles["apoC_annee"]}" """
) )
return int(m.group(0)) return int(m.group(0))
def write_header(self, f):
"""write apo CSV header on f
(beginning of CSV until columns titles just after XX-APO_VALEURS-XX line)
"""
f.write(self.header)
f.write(APO_NEWLINE)
f.write("XX-APO_VALEURS-XX" + APO_NEWLINE)
f.write(self.column_titles)
def write_etuds(self, f):
"""write apo CSV etuds on f"""
for e in self.etuds:
fields = [] # e['nip'], e['nom'], e['prenom'], e['naissance'] ]
for col_id in self.col_ids:
try:
fields.append(str(e.new_cols[col_id]))
except KeyError:
log(
"Error: %s %s missing column key %s"
% (e["nip"], e["nom"], col_id)
)
log("Details:\ne = %s" % pprint.pformat(e))
log("col_ids=%s" % pprint.pformat(self.col_ids))
log("etudiant ignore.\n")
f.write(APO_SEP.join(fields) + APO_NEWLINE)
def list_unknown_elements(self) -> list[str]: def list_unknown_elements(self) -> list[str]:
"""Liste des codes des elements Apogee non trouvés dans ScoDoc """Liste des codes des elements Apogee non trouvés dans ScoDoc
(après traitement de tous les étudiants) (après traitement de tous les étudiants)
""" """
codes = set() codes = set()
for e in self.etuds: for apo_etud in self.etuds:
codes.update({code for code in e.col_elts if e.col_elts[code] is None}) codes.update(
{code for code in apo_etud.col_elts if apo_etud.col_elts[code] is None}
)
codes_list = list(codes) codes_list = list(codes)
codes_list.sort() codes_list.sort()
return codes_list return codes_list
def list_elements(self): def list_elements(self) -> tuple[set[str], set[str]]:
"""Liste les codes des elements Apogée de la maquette """Liste les codes des elements Apogée de la maquette
et ceux des semestres ScoDoc associés et ceux des semestres ScoDoc associés
Retourne deux ensembles Retourne deux ensembles
""" """
try: try:
maq_elems = {self.cols[col_id]["Code"] for col_id in self.col_ids[4:]} maq_elems = {
except KeyError: self.apo_csv.cols[col_id]["Code"] for col_id in self.apo_csv.col_ids[4:]
}
except KeyError as exc:
# une colonne déclarée dans l'en-tête n'est pas présente # une colonne déclarée dans l'en-tête n'est pas présente
declared = self.col_ids[4:] # id des colones dans l'en-tête declared = self.apo_csv.col_ids[4:] # id des colones dans l'en-tête
present = sorted(self.cols.keys()) # colones presentes present = sorted(self.apo_csv.cols.keys()) # colonnes présentes
log("Fichier Apogee invalide:") log("Fichier Apogee invalide:")
log(f"Colonnes declarees: {declared}") log(f"Colonnes declarees: {declared}")
log(f"Colonnes presentes: {present}") log(f"Colonnes presentes: {present}")
raise ScoFormatError( raise ScoFormatError(
f"""Fichier Apogee invalide<br>Colonnes declarees: <tt>{declared}</tt> f"""Fichier Apogee invalide<br>Colonnes declarees: <tt>{declared}</tt>
<br>Colonnes presentes: <tt>{present}</tt>""" <br>Colonnes presentes: <tt>{present}</tt>"""
) ) from exc
# l'ensemble de tous les codes des elements apo des semestres: # l'ensemble de tous les codes des elements apo des semestres:
sem_elems = reduce(set.union, list(self.get_codes_by_sem().values()), set()) sem_elems = reduce(set.union, list(self.get_codes_by_sem().values()), set())
@ -1112,8 +845,8 @@ class ApoData:
) )
s = set() s = set()
codes_by_sem[sem["formsemestre_id"]] = s codes_by_sem[sem["formsemestre_id"]] = s
for col_id in self.col_ids[4:]: for col_id in self.apo_csv.col_ids[4:]:
code = self.cols[col_id]["Code"] # 'V1RT' code = self.apo_csv.cols[col_id]["Code"] # 'V1RT'
# associé à l'étape, l'année ou le semestre: # associé à l'étape, l'année ou le semestre:
if code in codes_semestre: if code in codes_semestre:
s.add(code) s.add(code)
@ -1130,22 +863,22 @@ class ApoData:
def build_cr_table(self): def build_cr_table(self):
"""Table compte rendu des décisions""" """Table compte rendu des décisions"""
CR = [] # tableau compte rendu des decisions rows = [] # tableau compte rendu des decisions
for e in self.etuds: for apo_etud in self.etuds:
cr = { cr = {
"NIP": e["nip"], "NIP": apo_etud["nip"],
"nom": e["nom"], "nom": apo_etud["nom"],
"prenom": e["prenom"], "prenom": apo_etud["prenom"],
"est_NAR": e.is_NAR, "est_NAR": apo_etud.is_nar,
"commentaire": "; ".join(e.log), "commentaire": "; ".join(apo_etud.log),
} }
if e.col_elts and e.col_elts[self.etape_apogee] is not None: if apo_etud.col_elts and apo_etud.col_elts[self.etape_apogee] is not None:
cr["etape"] = e.col_elts[self.etape_apogee].get("R", "") cr["etape"] = apo_etud.col_elts[self.etape_apogee].get("R", "")
cr["etape_note"] = e.col_elts[self.etape_apogee].get("N", "") cr["etape_note"] = apo_etud.col_elts[self.etape_apogee].get("N", "")
else: else:
cr["etape"] = "" cr["etape"] = ""
cr["etape_note"] = "" cr["etape_note"] = ""
CR.append(cr) rows.append(cr)
columns_ids = ["NIP", "nom", "prenom"] columns_ids = ["NIP", "nom", "prenom"]
columns_ids.extend(("etape", "etape_note", "est_NAR", "commentaire")) columns_ids.extend(("etape", "etape_note", "est_NAR", "commentaire"))
@ -1153,102 +886,12 @@ class ApoData:
T = GenTable( T = GenTable(
columns_ids=columns_ids, columns_ids=columns_ids,
titles=dict(zip(columns_ids, columns_ids)), titles=dict(zip(columns_ids, columns_ids)),
rows=CR, rows=rows,
xls_sheet_name="Decisions ScoDoc", xls_sheet_name="Decisions ScoDoc",
) )
return T return T
def _apo_read_cols(f):
"""Lecture colonnes apo :
Démarre après la balise XX-APO_COLONNES-XX
et s'arrête après la balise APO_COL_VAL_FIN
Colonne Apogee: les champs sont données par la ligne
apoL_a01_code de la section XX-APO_COLONNES-XX
col_id est apoL_c0001, apoL_c0002, ...
:return: { col_id : { title : value } }
Example: { 'apoL_c0001' : { 'Type Objet' : 'VET', 'Code' : 'V1IN', ... }, ... }
"""
line = f.readline().strip(" " + APO_NEWLINE)
fields = line.split(APO_SEP)
if fields[0] != "apoL_a01_code":
raise ScoFormatError(f"invalid line: {line} (expecting apoL_a01_code)")
col_keys = fields
while True: # skip premiere partie (apoL_a02_nom, ...)
line = f.readline().strip(" " + APO_NEWLINE)
if line == "APO_COL_VAL_DEB":
break
# après APO_COL_VAL_DEB
cols = {}
i = 0
while True:
line = f.readline().strip(" " + APO_NEWLINE)
if line == "APO_COL_VAL_FIN":
break
i += 1
fields = line.split(APO_SEP)
# sanity check
col_id = fields[0] # apoL_c0001, ...
if col_id in cols:
raise ScoFormatError(f"duplicate column definition: {col_id}")
m = re.match(r"^apoL_c([0-9]{4})$", col_id)
if not m:
raise ScoFormatError(
f"invalid column id: {line} (expecting apoL_c{col_id})"
)
if int(m.group(1)) != i:
raise ScoFormatError(f"invalid column id: {col_id} for index {i}")
cols[col_id] = DictCol(list(zip(col_keys, fields)))
cols[col_id].lineno = f.lineno # for debuging purpose
return cols
def _apo_read_TITRES(f) -> dict:
"Lecture section TITRES du fichier Apogée, renvoie dict"
d = {}
while True:
line = f.readline().strip(
" " + APO_NEWLINE
) # ne retire pas le \t (pour les clés vides)
if not line.strip(): # stoppe sur ligne pleines de \t
break
fields = line.split(APO_SEP)
if len(fields) == 2:
k, v = fields
else:
log(f"Error read CSV: \nline={line}\nfields={fields}")
log(dir(f))
raise ScoFormatError(
f"Fichier Apogee incorrect (section titres, {len(fields)} champs au lieu de 2)"
)
d[k] = v
#
if not d.get("apoC_Fichier_Exp", None):
raise ScoFormatError("Fichier Apogee incorrect: pas de titre apoC_Fichier_Exp")
# keep only basename: may be a windows or unix pathname
s = d["apoC_Fichier_Exp"].split("/")[-1]
s = s.split("\\")[-1] # for DOS paths, eg C:\TEMP\VL4RT_V3ASR.TXT
d["apoC_Fichier_Exp"] = s
return d
def _apo_skip_section(f):
"Saute section Apo: s'arrete apres ligne vide"
while True:
line = f.readline().strip()
if not line:
break
# -------------------------------------
def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]: def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
""" """
:param etape_apogee: etape (string or ApoEtapeVDI) :param etape_apogee: etape (string or ApoEtapeVDI)
@ -1260,13 +903,13 @@ def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
) )
def nar_etuds_table(apo_data, NAR_Etuds): def nar_etuds_table(apo_data, nar_etuds):
"""Liste les NAR -> excel table""" """Liste les NAR -> excel table"""
code_etape = apo_data.etape_apogee code_etape = apo_data.etape_apogee
today = datetime.datetime.today().strftime("%d/%m/%y") today = datetime.datetime.today().strftime("%d/%m/%y")
rows = [] rows = []
NAR_Etuds.sort(key=lambda k: k["nom"]) nar_etuds.sort(key=lambda k: k["nom"])
for e in NAR_Etuds: for e in nar_etuds:
rows.append( rows.append(
{ {
"nom": e["nom"], "nom": e["nom"],
@ -1345,29 +988,31 @@ def export_csv_to_apogee(
export_res_rat=export_res_rat, export_res_rat=export_res_rat,
) )
apo_data.setup() # -> .sems_etape apo_data.setup() # -> .sems_etape
apo_csv = apo_data.apo_csv
for e in apo_data.etuds: for apo_etud in apo_data.etuds:
e.is_apc = apo_data.is_apc apo_etud.is_apc = apo_data.is_apc
e.lookup_scodoc(apo_data.etape_formsemestre_ids) apo_etud.lookup_scodoc(apo_data.etape_formsemestre_ids)
e.associate_sco(apo_data) apo_etud.associate_sco(apo_data)
# Ré-écrit le fichier Apogée # Ré-écrit le fichier Apogée
f = io.StringIO() csv_data = apo_csv.write(apo_data.etuds)
apo_data.write_header(f)
apo_data.write_etuds(f)
# Table des NAR: # Table des NAR:
NAR_Etuds = [e for e in apo_data.etuds if e.is_NAR] nar_etuds = [apo_etud for apo_etud in apo_data.etuds if apo_etud.is_nar]
if NAR_Etuds: if nar_etuds:
nar_xls = nar_etuds_table(apo_data, NAR_Etuds) nar_xls = nar_etuds_table(apo_data, nar_etuds)
else: else:
nar_xls = None nar_xls = None
# Journaux & Comptes-rendus # Journaux & Comptes-rendus
# Orphelins: etudiants dans fichier Apogée mais pas dans ScoDoc # Orphelins: etudiants dans fichier Apogée mais pas dans ScoDoc
Apo_Non_ScoDoc = [e for e in apo_data.etuds if e.etat == ETUD_ORPHELIN] apo_non_scodoc = [
apo_etud for apo_etud in apo_data.etuds if apo_etud.etat == ETUD_ORPHELIN
]
# Non inscrits: connus de ScoDoc mais pas inscrit dans l'étape cette année # Non inscrits: connus de ScoDoc mais pas inscrit dans l'étape cette année
Apo_Non_ScoDoc_Inscrits = [e for e in apo_data.etuds if e.etat == ETUD_NON_INSCRIT] apo_non_scodoc_inscrits = [
apo_etud for apo_etud in apo_data.etuds if apo_etud.etat == ETUD_NON_INSCRIT
]
# CR table # CR table
cr_table = apo_data.build_cr_table() cr_table = apo_data.build_cr_table()
cr_xls = cr_table.excel() cr_xls = cr_table.excel()
@ -1380,19 +1025,19 @@ def export_csv_to_apogee(
else: else:
my_zip = False my_zip = False
# Ensure unique filenames # Ensure unique filenames
filename = apo_data.titles["apoC_Fichier_Exp"] filename = apo_csv.get_filename()
basename, ext = os.path.splitext(filename) basename, ext = os.path.splitext(filename)
csv_filename = filename csv_filename = filename
if csv_filename in dest_zip.namelist(): if csv_filename in dest_zip.namelist():
basename = filename + "-" + apo_data.vdi_apogee basename = filename + "-" + apo_data.vdi_apogee
csv_filename = basename + ext csv_filename = basename + ext
nf = 1 num_file = 1
tmplname = basename tmplname = basename
while csv_filename in dest_zip.namelist(): while csv_filename in dest_zip.namelist():
basename = tmplname + "-%d" % nf basename = f"{tmplname}-{num_file}"
csv_filename = basename + ext csv_filename = basename + ext
nf += 1 num_file += 1
log_filename = "scodoc-" + basename + ".log.txt" log_filename = "scodoc-" + basename + ".log.txt"
nar_filename = basename + "-nar" + scu.XLSX_SUFFIX nar_filename = basename + "-nar" + scu.XLSX_SUFFIX
@ -1416,7 +1061,7 @@ def export_csv_to_apogee(
logf.write( logf.write(
"\nÉtudiants Apogée non trouvés dans ScoDoc:\n" "\nÉtudiants Apogée non trouvés dans ScoDoc:\n"
+ "\n".join( + "\n".join(
["%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"]) for e in Apo_Non_ScoDoc] ["%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"]) for e in apo_non_scodoc]
) )
) )
logf.write( logf.write(
@ -1424,7 +1069,7 @@ def export_csv_to_apogee(
+ "\n".join( + "\n".join(
[ [
"%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"]) "%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"])
for e in Apo_Non_ScoDoc_Inscrits for e in apo_non_scodoc_inscrits
] ]
) )
) )
@ -1435,8 +1080,6 @@ def export_csv_to_apogee(
) )
log(logf.getvalue()) # sortie aussi sur le log ScoDoc log(logf.getvalue()) # sortie aussi sur le log ScoDoc
csv_data = f.getvalue().encode(APO_OUTPUT_ENCODING)
# Write data to ZIP # Write data to ZIP
dest_zip.writestr(csv_filename, csv_data) dest_zip.writestr(csv_filename, csv_data)
dest_zip.writestr(log_filename, logf.getvalue()) dest_zip.writestr(log_filename, logf.getvalue())

View File

@ -0,0 +1,483 @@
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Lecture du fichier "maquette" Apogée
Le fichier CSV, champs séparés par des tabulations, a la structure suivante:
<pre>
XX-APO_TITRES-XX
apoC_annee 2007/2008
apoC_cod_dip VDTCJ
apoC_Cod_Exp 1
apoC_cod_vdi 111
apoC_Fichier_Exp VDTCJ_V1CJ.txt
apoC_lib_dip DUT CJ
apoC_Titre1 Export Apogée du 13/06/2008 à 14:29
apoC_Titre2
XX-APO_TYP_RES-XX
...section optionnelle au contenu quelconque...
XX-APO_COLONNES-XX
apoL_a01_code Type Objet Code Version Année Session Admission/Admissibilité Type Rés. Etudiant Numéro
apoL_a02_nom 1 Nom
apoL_a03_prenom 1 Prénom
apoL_a04_naissance Session Admissibilité Naissance
APO_COL_VAL_DEB
apoL_c0001 VET V1CJ 111 2007 0 1 N V1CJ - DUT CJ an1 0 1 Note
apoL_c0002 VET V1CJ 111 2007 0 1 B 0 1 Barème
apoL_c0003 VET V1CJ 111 2007 0 1 R 0 1 Résultat
APO_COL_VAL_FIN
apoL_c0030 APO_COL_VAL_FIN
XX-APO_VALEURS-XX
apoL_a01_code apoL_a02_nom apoL_a03_prenom apoL_a04_naissance apoL_c0001 apoL_c0002 apoL_c0003 apoL_c0004 apoL_c0005 apoL_c0006 apoL_c0007 apoL_c0008 apoL_c0009 apoL_c0010 apoL_c0011 apoL_c0012 apoL_c0013 apoL_c0014 apoL_c0015 apoL_c0016 apoL_c0017 apoL_c0018 apoL_c0019 apoL_c0020 apoL_c0021 apoL_c0022 apoL_c0023 apoL_c0024 apoL_c0025 apoL_c0026 apoL_c0027 apoL_c0028 apoL_c0029
10601232 AARIF MALIKA 22/09/1986 18 20 ADM 18 20 ADM 18 20 ADM 18 20 ADM 18 20 ADM 18 20 18 20 ADM 18 20 ADM 18 20 ADM 18 20 ADM
</pre>
On récupère nos éléments pédagogiques dans la section XX-APO-COLONNES-XX et
notre liste d'étudiants dans la section XX-APO_VALEURS-XX. Les champs de la
section XX-APO_VALEURS-XX sont décrits par les lignes successives de la
section XX-APO_COLONNES-XX.
Le fichier CSV correspond à une étape, qui est récupérée sur la ligne
<pre>
apoL_c0001 VET V1CJ ...
</pre>
"""
from collections import namedtuple
import io
import pprint
import re
# Pour la détection auto de l'encodage des fichiers Apogée:
from chardet import detect as chardet_detect
from app import log
from app.scodoc.sco_exceptions import ScoFormatError
APO_PORTAL_ENCODING = (
"utf8" # encodage du fichier CSV Apogée (était 'ISO-8859-1' avant jul. 2016)
)
APO_INPUT_ENCODING = "ISO-8859-1" #
APO_OUTPUT_ENCODING = APO_INPUT_ENCODING # encodage des fichiers Apogee générés
APO_DECIMAL_SEP = "," # separateur décimal: virgule
APO_SEP = "\t"
APO_NEWLINE = "\r\n"
ApoEtudTuple = namedtuple("ApoEtudTuple", ("nip", "nom", "prenom", "naissance", "cols"))
class DictCol(dict):
"A dict, where we can add attributes"
class StringIOWithLineNumber(io.StringIO):
"simple wrapper to use a string as a file with line numbers"
def __init__(self, data: str):
super().__init__(data)
self.lineno = 0
def readline(self):
self.lineno += 1
return super().readline()
class ApoCSVReadWrite:
"Gestion lecture/écriture de fichiers csv Apogée"
def __init__(self, data: str):
if not data:
raise ScoFormatError("Fichier Apogée vide !")
self.data = data
self._file = StringIOWithLineNumber(data) # pour traiter comme un fichier
self.apo_elts: dict = None
self.cols: dict[str, dict[str, str]] = None
self.column_titles: str = None
self.col_ids: list[str] = None
self.csv_etuds: list[ApoEtudTuple] = []
# section_str: utilisé pour ré-écrire les headers sans aucune altération
self.sections_str: dict[str, str] = {}
"contenu initial de chaque section"
# self.header: str = ""
# "début du fichier Apogée jusqu'à XX-APO_TYP_RES-XX non inclu (sera ré-écrit non modifié)"
self.header_apo_typ_res: str = ""
"section XX-APO_TYP_RES-XX (qui peut en option ne pas être ré-écrite)"
self.titles: dict[str, str] = {}
"titres Apogée (section XX-APO_TITRES-XX)"
self.read_sections()
# Check that we have collected all requested infos:
if not self.header_apo_typ_res:
# on pourrait rendre XX-APO_TYP_RES-XX optionnelle mais mieux vaut vérifier:
raise ScoFormatError(
"format incorrect: pas de XX-APO_TYP_RES-XX",
filename=self.get_filename(),
)
if self.cols is None:
raise ScoFormatError(
"format incorrect: pas de XX-APO_COLONNES-XX",
filename=self.get_filename(),
)
if self.column_titles is None:
raise ScoFormatError(
"format incorrect: pas de XX-APO_VALEURS-XX",
filename=self.get_filename(),
)
def read_sections(self):
"""Lit une à une les sections du fichier Apogée"""
# sanity check: we are at the begining of Apogee CSV
start_pos = self._file.tell()
section = self._file.readline().strip()
if section != "XX-APO_TITRES-XX":
raise ScoFormatError("format incorrect: pas de XX-APO_TITRES-XX")
while True:
self.read_section(section)
line, end_pos = _apo_next_non_blank_line(self._file)
self.sections_str[section] = self.data[start_pos:end_pos]
if not line:
break
section = line
start_pos = end_pos
def read_section(self, section_name: str):
"""Read a section: _file is on the first line after section title"""
if section_name == "XX-APO_TITRES-XX":
# Titres:
# on va y chercher apoC_Fichier_Exp qui donnera le nom du fichier
# ainsi que l'année scolaire et le code diplôme.
self.titles = self._apo_read_titres(self._file)
elif section_name == "XX-APO_TYP_RES-XX":
self.header_apo_typ_res = _apo_read_typ_res(self._file)
elif section_name == "XX-APO_COLONNES-XX":
self.cols = self.apo_read_cols()
self.apo_elts = self.group_elt_cols(self.cols)
elif section_name == "XX-APO_VALEURS-XX":
# les étudiants
self.apo_read_section_valeurs()
else:
raise ScoFormatError(
f"format incorrect: section inconnue: {section_name}",
filename=self.get_filename(),
)
def apo_read_cols(self):
"""Lecture colonnes apo :
Démarre après la balise XX-APO_COLONNES-XX
et s'arrête après la ligne suivant la balise APO_COL_VAL_FIN
Colonne Apogee: les champs sont données par la ligne
apoL_a01_code de la section XX-APO_COLONNES-XX
col_id est apoL_c0001, apoL_c0002, ...
:return: { col_id : { title : value } }
Example: { 'apoL_c0001' : { 'Type Objet' : 'VET', 'Code' : 'V1IN', ... }, ... }
"""
line = self._file.readline().strip(" " + APO_NEWLINE)
fields = line.split(APO_SEP)
if fields[0] != "apoL_a01_code":
raise ScoFormatError(
f"invalid line: {line} (expecting apoL_a01_code)",
filename=self.get_filename(),
)
col_keys = fields
while True: # skip premiere partie (apoL_a02_nom, ...)
line = self._file.readline().strip(" " + APO_NEWLINE)
if line == "APO_COL_VAL_DEB":
break
# après APO_COL_VAL_DEB
cols = {}
i = 0
while True:
line = self._file.readline().strip(" " + APO_NEWLINE)
if line == "APO_COL_VAL_FIN":
break
i += 1
fields = line.split(APO_SEP)
# sanity check
col_id = fields[0] # apoL_c0001, ...
if col_id in cols:
raise ScoFormatError(
f"duplicate column definition: {col_id}",
filename=self.get_filename(),
)
m = re.match(r"^apoL_c([0-9]{4})$", col_id)
if not m:
raise ScoFormatError(
f"invalid column id: {line} (expecting apoL_c{col_id})",
filename=self.get_filename(),
)
if int(m.group(1)) != i:
raise ScoFormatError(
f"invalid column id: {col_id} for index {i}",
filename=self.get_filename(),
)
cols[col_id] = DictCol(list(zip(col_keys, fields)))
cols[col_id].lineno = self._file.lineno # for debuging purpose
self._file.readline() # skip next line
return cols
def group_elt_cols(self, cols) -> dict:
"""Return (ordered) dict of ApoElt from list of ApoCols.
Clé: id apogée, eg 'V1RT', 'V1GE2201', ...
Valeur: ApoElt, avec les attributs code, type_objet
Si les id Apogée ne sont pas uniques (ce n'est pas garanti), garde le premier
"""
elts = {}
for col_id in sorted(list(cols.keys()), reverse=True):
col = cols[col_id]
if col["Code"] in elts:
elts[col["Code"]].append(col)
else:
elts[col["Code"]] = ApoElt([col])
return elts # { code apo : ApoElt }
def apo_read_section_valeurs(self):
"traitement de la section XX-APO_VALEURS-XX"
self.column_titles = self._file.readline()
self.col_ids = self.column_titles.strip().split()
self.csv_etuds = self.apo_read_etuds()
def apo_read_etuds(self) -> list[ApoEtudTuple]:
"""Lecture des étudiants (et résultats) du fichier CSV Apogée.
Les lignes "étudiant" commencent toujours par
`12345678 NOM PRENOM 15/05/2003`
le premier code étant le NIP.
"""
etud_tuples = []
while True:
line = self._file.readline()
# cette section est impérativement la dernière du fichier
# donc on arrête ici:
if not line:
break
if not line.strip():
continue # silently ignore blank lines
line = line.strip(APO_NEWLINE)
fields = line.split(APO_SEP)
if len(fields) < 4:
raise ScoFormatError(
"""Ligne étudiant invalide
(doit commencer par 'NIP NOM PRENOM dd/mm/yyyy')""",
filename=self.get_filename(),
)
cols = {} # { col_id : value }
for i, field in enumerate(fields):
cols[self.col_ids[i]] = field
etud_tuples.append(
ApoEtudTuple(
nip=fields[0], # id etudiant
nom=fields[1],
prenom=fields[2],
naissance=fields[3],
cols=cols,
)
# XXX à remettre dans apogee_csv.py
# export_res_etape=self.export_res_etape,
# export_res_sem=self.export_res_sem,
# export_res_ues=self.export_res_ues,
# export_res_modules=self.export_res_modules,
# export_res_sdj=self.export_res_sdj,
# export_res_rat=self.export_res_rat,
# )
)
return etud_tuples
def _apo_read_titres(self, f) -> dict:
"Lecture section TITRES du fichier Apogée, renvoie dict"
d = {}
while True:
line = f.readline().strip(
" " + APO_NEWLINE
) # ne retire pas le \t (pour les clés vides)
if not line.strip(): # stoppe sur ligne pleines de \t
break
fields = line.split(APO_SEP)
if len(fields) == 2:
k, v = fields
else:
log(f"Error read CSV: \nline={line}\nfields={fields}")
log(dir(f))
raise ScoFormatError(
f"Fichier Apogee incorrect (section titres, {len(fields)} champs au lieu de 2)",
filename=self.get_filename(),
)
d[k] = v
#
if not d.get("apoC_Fichier_Exp", None):
raise ScoFormatError(
"Fichier Apogee incorrect: pas de titre apoC_Fichier_Exp",
filename=self.get_filename(),
)
# keep only basename: may be a windows or unix pathname
s = d["apoC_Fichier_Exp"].split("/")[-1]
s = s.split("\\")[-1] # for DOS paths, eg C:\TEMP\VL4RT_V3ASR.TXT
d["apoC_Fichier_Exp"] = s
return d
def get_filename(self) -> str:
"""Le nom du fichier APogée, tel qu'indiqué dans le fichier
ou vide."""
if self.titles:
return self.titles.get("apoC_Fichier_Exp", "")
return ""
def write(self, apo_etuds: list["ApoEtud"]) -> bytes:
"""Renvoie le contenu actualisé du fichier Apogée"""
f = io.StringIO()
self._write_header(f)
self._write_etuds(f, apo_etuds)
return f.getvalue().encode(APO_OUTPUT_ENCODING)
def _write_etuds(self, f, apo_etuds: list["ApoEtud"]):
"""write apo CSV etuds on f"""
for apo_etud in apo_etuds:
fields = [] # e['nip'], e['nom'], e['prenom'], e['naissance'] ]
for col_id in self.col_ids:
try:
fields.append(str(apo_etud.new_cols[col_id]))
except KeyError:
log(
f"""Error: {apo_etud["nip"]} {apo_etud["nom"]} missing column key {col_id}
Details:\napo_etud = {pprint.pformat(apo_etud)}
col_ids={pprint.pformat(self.col_ids)}
étudiant ignoré.
"""
)
f.write(APO_SEP.join(fields) + APO_NEWLINE)
def _write_header(self, f):
"""write apo CSV header on f
(beginning of CSV until columns titles just after XX-APO_VALEURS-XX line)
"""
for section, data in self.sections_str.items():
if section != "XX-APO_VALEURS-XX":
# XXX TODO ici on va filtrer XX-APO_TYP_RES-XX
f.write(data)
f.write("XX-APO_VALEURS-XX" + APO_NEWLINE)
f.write(self.column_titles)
class ApoElt:
"""Définition d'un Element Apogée
sur plusieurs colonnes du fichier CSV
"""
def __init__(self, cols):
assert len(cols) > 0
assert len(set([c["Code"] for c in cols])) == 1 # colonnes de meme code
assert len(set([c["Type Objet"] for c in cols])) == 1 # colonnes de meme type
self.cols = cols
self.code = cols[0]["Code"]
self.version = cols[0]["Version"]
self.type_objet = cols[0]["Type Objet"]
def append(self, col):
"""ajoute une "colonne" à l'élément"""
assert col["Code"] == self.code
if col["Type Objet"] != self.type_objet:
log(
f"""Warning: ApoElt: duplicate id {
self.code} ({self.type_objet} and {col["Type Objet"]})"""
)
self.type_objet = col["Type Objet"]
self.cols.append(col)
def __repr__(self):
return f"ApoElt(code='{self.code}', cols={pprint.pformat(self.cols)})"
def guess_data_encoding(text: bytes, threshold=0.6):
"""Guess string encoding, using chardet heuristics.
Returns encoding, or None if detection failed (confidence below threshold)
"""
r = chardet_detect(text)
if r["confidence"] < threshold:
return None
else:
return r["encoding"]
def fix_data_encoding(
text: bytes,
default_source_encoding=APO_INPUT_ENCODING,
dest_encoding=APO_INPUT_ENCODING,
) -> tuple[bytes, str]:
"""Try to ensure that text is using dest_encoding
returns converted text, and a message describing the conversion.
Raises UnicodeEncodeError en cas de problème, en général liée à
une auto-détection errornée.
"""
message = ""
detected_encoding = guess_data_encoding(text)
if not detected_encoding:
if default_source_encoding != dest_encoding:
message = f"converting from {default_source_encoding} to {dest_encoding}"
text = text.decode(default_source_encoding).encode(dest_encoding)
else:
if detected_encoding != dest_encoding:
message = (
f"converting from detected {default_source_encoding} to {dest_encoding}"
)
text = text.decode(detected_encoding).encode(dest_encoding)
return text, message
def _apo_read_typ_res(f) -> str:
"Lit la section XX-APO_TYP_RES-XX"
text = "XX-APO_TYP_RES-XX" + APO_NEWLINE
while True:
line = f.readline()
stripped_line = line.strip()
if not stripped_line:
break
text += line
return text
def _apo_next_non_blank_line(f: StringIOWithLineNumber) -> tuple[str, int]:
"Ramène prochaine ligne non blanche, stripped, et l'indice de son début"
while True:
pos = f.tell()
line = f.readline()
if not line:
return "", -1
stripped_line = line.strip()
if stripped_line:
return stripped_line, pos

View File

@ -76,7 +76,7 @@ import re
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_archives from app.scodoc import sco_archives
from app.scodoc import sco_apogee_csv from app.scodoc import sco_apogee_csv, sco_apogee_reader
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -108,7 +108,7 @@ def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
# sanity check # sanity check
filesize = len(csv_data) filesize = len(csv_data)
if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE: if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE:
raise ScoValueError("Fichier csv de taille invalide ! (%d)" % filesize) raise ScoValueError(f"Fichier csv de taille invalide ! ({filesize})")
if not annee_scolaire: if not annee_scolaire:
raise ScoValueError("Impossible de déterminer l'année scolaire !") raise ScoValueError("Impossible de déterminer l'année scolaire !")
@ -121,13 +121,13 @@ def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
if str(apo_data.etape) in apo_csv_list_stored_etapes(annee_scolaire, sem_id=sem_id): if str(apo_data.etape) in apo_csv_list_stored_etapes(annee_scolaire, sem_id=sem_id):
raise ScoValueError( raise ScoValueError(
"Etape %s déjà stockée pour cette année scolaire !" % apo_data.etape f"Etape {apo_data.etape} déjà stockée pour cette année scolaire !"
) )
oid = "%d-%d" % (annee_scolaire, sem_id) oid = f"{annee_scolaire}-{sem_id}"
description = "%s;%s;%s" % (str(apo_data.etape), annee_scolaire, sem_id) description = f"""{str(apo_data.etape)};{annee_scolaire};{sem_id}"""
archive_id = ApoCSVArchive.create_obj_archive(oid, description) archive_id = ApoCSVArchive.create_obj_archive(oid, description)
csv_data_bytes = csv_data.encode(sco_apogee_csv.APO_OUTPUT_ENCODING) csv_data_bytes = csv_data.encode(sco_apogee_reader.APO_OUTPUT_ENCODING)
ApoCSVArchive.store(archive_id, filename, csv_data_bytes) ApoCSVArchive.store(archive_id, filename, csv_data_bytes)
return apo_data.etape return apo_data.etape
@ -212,7 +212,7 @@ def apo_csv_get(etape_apo="", annee_scolaire="", sem_id="") -> str:
data = ApoCSVArchive.get(archive_id, etape_apo + ".csv") data = ApoCSVArchive.get(archive_id, etape_apo + ".csv")
# ce fichier a été archivé donc généré par ScoDoc # ce fichier a été archivé donc généré par ScoDoc
# son encodage est donc APO_OUTPUT_ENCODING # son encodage est donc APO_OUTPUT_ENCODING
return data.decode(sco_apogee_csv.APO_OUTPUT_ENCODING) return data.decode(sco_apogee_reader.APO_OUTPUT_ENCODING)
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------

View File

@ -32,13 +32,13 @@ import io
from zipfile import ZipFile from zipfile import ZipFile
import flask import flask
from flask import flash, g, request, send_file, url_for from flask import flash, g, request, Response, send_file, url_for
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.models import Formation from app.models import Formation
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_apogee_csv from app.scodoc import sco_apogee_csv, sco_apogee_reader
from app.scodoc import sco_etape_apogee from app.scodoc import sco_etape_apogee
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
@ -46,7 +46,7 @@ from app.scodoc import sco_preferences
from app.scodoc import sco_semset from app.scodoc import sco_semset
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_apogee_csv import APO_INPUT_ENCODING, APO_OUTPUT_ENCODING from app.scodoc.sco_apogee_reader import APO_INPUT_ENCODING, APO_OUTPUT_ENCODING
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -279,11 +279,10 @@ def apo_semset_maq_status(
if semset and ok_for_export: if semset and ok_for_export:
H.append( H.append(
"""<form class="form_apo_export" action="apo_csv_export_results" method="get"> f"""<form class="form_apo_export" action="apo_csv_export_results" method="get">
<input type="submit" value="Export vers Apogée"> <input type="submit" value="Export vers Apogée">
<input type="hidden" name="semset_id" value="%s"/> <input type="hidden" name="semset_id" value="{semset_id}"/>
""" """
% (semset_id,)
) )
H.append('<div id="param_export_res">') H.append('<div id="param_export_res">')
@ -376,7 +375,7 @@ def apo_semset_maq_status(
H.append("</div>") H.append("</div>")
# Aide: # Aide:
H.append( H.append(
""" f"""
<p><a class="stdlink" href="semset_page">Retour aux ensembles de semestres</a></p> <p><a class="stdlink" href="semset_page">Retour aux ensembles de semestres</a></p>
<div class="pas_help"> <div class="pas_help">
@ -385,10 +384,12 @@ def apo_semset_maq_status(
l'export des résultats après les jurys, puis de remplir et exporter ces fichiers. l'export des résultats après les jurys, puis de remplir et exporter ces fichiers.
</p> </p>
<p> <p>
Les fichiers ("maquettes") Apogée sont de type CSV, du texte codé en %s. Les fichiers ("maquettes") Apogée sont de type CSV, du texte codé en {APO_INPUT_ENCODING}.
</p>
<p>On a un fichier par étape Apogée. Pour les obtenir, soit on peut les télécharger
directement (si votre ScoDoc est interfacé avec Apogée), soit se débrouiller pour
exporter le fichier texte depuis Apogée. Son contenu ressemble à cela:
</p> </p>
<p>On a un fichier par étape Apogée. Pour les obtenir, soit on peut les télécharger directement (si votre ScoDoc est interfacé avec Apogée), soit se débrouiller pour exporter le fichier
texte depuis Apogée. Son contenu ressemble à cela:</p>
<pre class="small_pre_acc"> <pre class="small_pre_acc">
XX-APO_TITRES-XX XX-APO_TITRES-XX
apoC_annee 2007/2008 apoC_annee 2007/2008
@ -431,7 +432,6 @@ def apo_semset_maq_status(
</p> </p>
</div> </div>
""" """
% (APO_INPUT_ENCODING,)
) )
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
return "\n".join(H) return "\n".join(H)
@ -450,21 +450,25 @@ def table_apo_csv_list(semset):
# 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"])
t["filename"] = apo_data.titles["apoC_Fichier_Exp"] t["filename"] = apo_data.apo_csv.titles["apoC_Fichier_Exp"]
t["nb_etuds"] = len(apo_data.etuds) t["nb_etuds"] = len(apo_data.etuds)
t["date_str"] = t["date"].strftime("%d/%m/%Y à %H:%M") t["date_str"] = t["date"].strftime("%d/%m/%Y à %H:%M")
view_link = "view_apo_csv?etape_apo=%s&semset_id=%s" % ( view_link = url_for(
t["etape_apo"], "notes.view_apo_csv",
semset["semset_id"], scodoc_dept=g.scodoc_dept,
etape_apo=t["etape_apo"],
semset_id=semset["semset_id"],
) )
t["_filename_target"] = view_link t["_filename_target"] = view_link
t["_etape_apo_target"] = view_link t["_etape_apo_target"] = view_link
t["suppress"] = scu.icontag( t["suppress"] = scu.icontag(
"delete_small_img", border="0", alt="supprimer", title="Supprimer" "delete_small_img", border="0", alt="supprimer", title="Supprimer"
) )
t["_suppress_target"] = "view_apo_csv_delete?etape_apo=%s&semset_id=%s" % ( t["_suppress_target"] = url_for(
t["etape_apo"], "notes.view_apo_csv_delete",
semset["semset_id"], scodoc_dept=g.scodoc_dept,
etape_apo=t["etape_apo"],
semset_id=semset["semset_id"],
) )
columns_ids = ["filename", "etape_apo", "date_str", "nb_etuds"] columns_ids = ["filename", "etape_apo", "date_str", "nb_etuds"]
@ -508,13 +512,16 @@ def view_apo_etuds(semset_id, title="", nip_list="", format="html"):
for etud in etuds.values(): for etud in etuds.values():
etud_sco = sco_etud.get_etud_info(code_nip=etud["nip"], filled=True) etud_sco = sco_etud.get_etud_info(code_nip=etud["nip"], filled=True)
if etud_sco: if etud_sco:
e = etud_sco[0]
etud["inscriptions_scodoc"] = ", ".join( etud["inscriptions_scodoc"] = ", ".join(
[ [
'<a href="formsemestre_bulletinetud?formsemestre_id={s[formsemestre_id]}&etudid={e[etudid]}">{s[etapes_apo_str]} (S{s[semestre_id]})</a>'.format( f"""<a href="{
s=sem, e=e url_for('notes.formsemestre_bulletinetud',
) scodoc_dept=g.scodoc_dept,
for sem in e["sems"] formsemestre_id=sem["formsemestre_id"],
etudid=etud_sco[0]["etudid"])
}">{sem["etapes_apo_str"]} (S{sem["semestre_id"]})</a>
"""
for sem in etud_sco[0]["sems"]
] ]
) )
@ -538,8 +545,8 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"):
tgt = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]) tgt = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"])
e["_nom_target"] = tgt e["_nom_target"] = tgt
e["_prenom_target"] = tgt e["_prenom_target"] = tgt
e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],) e["_nom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """
e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],) e["_prenom_td_attrs"] = f"""id="pre-{e['etudid']}" class="etudinfo" """
return _view_etuds_page( return _view_etuds_page(
semset_id, semset_id,
@ -550,20 +557,14 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"):
) )
def _view_etuds_page(semset_id, title="", etuds=[], keys=(), format="html"): def _view_etuds_page(
semset_id: int, title="", etuds: list = None, keys=(), format="html"
) -> str:
"Affiche les étudiants indiqués"
# Tri les étudiants par nom: # Tri les étudiants par nom:
if etuds: if etuds: # XXX TODO modifier pour utiliser clé de tri
etuds.sort(key=lambda x: (x["nom"], x["prenom"])) etuds.sort(key=lambda x: (x["nom"], x["prenom"]))
H = [
html_sco_header.sco_header(
page_title=title,
init_qtip=True,
javascripts=["js/etud_info.js"],
),
"<h2>%s</h2>" % title,
]
tab = GenTable( tab = GenTable(
titles={ titles={
"nip": "Code NIP", "nip": "Code NIP",
@ -583,14 +584,23 @@ def _view_etuds_page(semset_id, title="", etuds=[], keys=(), format="html"):
if format != "html": if format != "html":
return tab.make_page(format=format) return tab.make_page(format=format)
H.append(tab.html()) return f"""
{html_sco_header.sco_header(
page_title=title,
init_qtip=True,
javascripts=["js/etud_info.js"],
)}
<h2>{title}</h2>
H.append( {tab.html()}
"""<p><a href="apo_semset_maq_status?semset_id=%s">Retour à la page d'export Apogée</a>"""
% semset_id
)
return "\n".join(H) + html_sco_header.sco_footer() <p><a href="{
url_for("notes.apo_semset_maq_status",
scodoc_dept=g.scodoc_dept, semset_id=semset_id)
}">Retour à la page d'export Apogée</a>
</p>
{html_sco_header.sco_footer()}
"""
def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=False): def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=False):
@ -607,7 +617,7 @@ def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=
if autodetect: if autodetect:
# check encoding (although documentation states that users SHOULD upload LATIN1) # check encoding (although documentation states that users SHOULD upload LATIN1)
data, message = sco_apogee_csv.fix_data_encoding(data) data, message = sco_apogee_reader.fix_data_encoding(data)
if message: if message:
log(f"view_apo_csv_store: {message}") log(f"view_apo_csv_store: {message}")
else: else:
@ -627,7 +637,7 @@ def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=
f""" f"""
Erreur: l'encodage du fichier est mal détecté. Erreur: l'encodage du fichier est mal détecté.
Essayez sans auto-détection, ou vérifiez le codage et le contenu Essayez sans auto-détection, ou vérifiez le codage et le contenu
du fichier (qui doit être en {sco_apogee_csv.APO_INPUT_ENCODING}). du fichier (qui doit être en {sco_apogee_reader.APO_INPUT_ENCODING}).
""", """,
dest_url=dest_url, dest_url=dest_url,
) from exc ) from exc
@ -635,7 +645,7 @@ def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=
raise ScoValueError( raise ScoValueError(
f""" f"""
Erreur: l'encodage du fichier est incorrect. Erreur: l'encodage du fichier est incorrect.
Vérifiez qu'il est bien en {sco_apogee_csv.APO_INPUT_ENCODING} Vérifiez qu'il est bien en {sco_apogee_reader.APO_INPUT_ENCODING}
""", """,
dest_url=dest_url, dest_url=dest_url,
) from exc ) from exc
@ -683,9 +693,8 @@ def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
dest_url = f"apo_semset_maq_status?semset_id={semset_id}" dest_url = f"apo_semset_maq_status?semset_id={semset_id}"
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2> f"""<h2>Confirmer la suppression du fichier étape <tt>{etape_apo}</tt>?</h2>
<p>La suppression sera définitive.</p>""" <p>La suppression sera définitive.</p>""",
% (etape_apo,),
dest_url="", dest_url="",
cancel_url=dest_url, cancel_url=dest_url,
parameters={"semset_id": semset_id, "etape_apo": etape_apo}, parameters={"semset_id": semset_id, "etape_apo": etape_apo},
@ -731,24 +740,24 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Maquette Apogée enregistrée pour %s" % etape_apo, page_title=f"""Maquette Apogée enregistrée pour {etape_apo}""",
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js"], javascripts=["js/etud_info.js"],
), ),
"""<h2>Etudiants dans la maquette Apogée %s</h2>""" % etape_apo, f"""<h2>Étudiants dans la maquette Apogée {etape_apo}</h2>
"""<p>Pour l'ensemble <a class="stdlink" href="apo_semset_maq_status?semset_id=%(semset_id)s">%(title)s</a> (indice semestre: %(sem_id)s)</p>""" <p>Pour l'ensemble <a class="stdlink" href="{
% semset, url_for("notes.apo_semset_maq_status",
scodoc_dept=g.scodoc_dept, semset_id=semset["semset_id"])
}">{semset['title']}</a> (indice semestre: {semset['sem_id']})
</p>
<div class="apo_csv_infos">
<div class="apo_csv_etape"><span>Code étape:</span><span>{
apo_data.etape_apogee} VDI {apo_data.vdi_apogee} (année {apo_data.annee_scolaire
})</span>
</div>
</div>
""",
] ]
# Infos générales
H.append(
"""
<div class="apo_csv_infos">
<div class="apo_csv_etape"><span>Code étape:</span><span>{0.etape_apogee} VDI {0.vdi_apogee} (année {0.annee_scolaire})</span></div>
</div>
""".format(
apo_data
)
)
# Liste des étudiants (sans les résultats pour le moment): TODO # Liste des étudiants (sans les résultats pour le moment): TODO
etuds = apo_data.etuds etuds = apo_data.etuds
@ -793,12 +802,21 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
return tab.make_page(format=format) return tab.make_page(format=format)
H += [ H += [
tab.html(), f"""
"""<p><a class="stdlink" href="view_apo_csv?etape_apo=%s&semset_id=%s&format=raw">fichier maquette CSV brut (non rempli par ScoDoc)</a></p>""" {tab.html()}
% (etape_apo, semset_id), <p><a class="stdlink" href="{
"""<div><a class="stdlink" href="apo_semset_maq_status?semset_id=%s">Retour</a> url_for("notes.view_apo_csv",
</div>""" scodoc_dept=g.scodoc_dept,
% semset_id, etape_apo=etape_apo, semset_id=semset_id, format="raw")
}">fichier maquette CSV brut (non rempli par ScoDoc)</a>
</p>
<div>
<a class="stdlink" href="{
url_for("notes.apo_semset_maq_status",
scodoc_dept=g.scodoc_dept, semset_id=semset_id)
}">Retour</a>
</div>
""",
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
@ -813,7 +831,7 @@ def apo_csv_export_results(
block_export_res_ues=False, block_export_res_ues=False,
block_export_res_modules=False, block_export_res_modules=False,
block_export_res_sdj=False, block_export_res_sdj=False,
): ) -> Response:
"""Remplit les fichiers CSV archivés """Remplit les fichiers CSV archivés
et donne un ZIP avec tous les résultats. et donne un ZIP avec tous les résultats.
""" """
@ -837,31 +855,28 @@ def apo_csv_export_results(
periode = semset["sem_id"] periode = semset["sem_id"]
data = io.BytesIO() data = io.BytesIO()
dest_zip = ZipFile(data, "w") with ZipFile(data, "w") as dest_zip:
etapes_apo = sco_etape_apogee.apo_csv_list_stored_etapes(
etapes_apo = sco_etape_apogee.apo_csv_list_stored_etapes( annee_scolaire, periode, etapes=semset.list_etapes()
annee_scolaire, periode, etapes=semset.list_etapes()
)
for etape_apo in etapes_apo:
apo_csv = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, periode)
sco_apogee_csv.export_csv_to_apogee(
apo_csv,
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,
dest_zip=dest_zip,
) )
for etape_apo in etapes_apo:
apo_csv = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, periode)
sco_apogee_csv.export_csv_to_apogee(
apo_csv,
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,
dest_zip=dest_zip,
)
dest_zip.close()
data.seek(0) data.seek(0)
basename = ( basename = (
sco_preferences.get_preference("DeptName") sco_preferences.get_preference("DeptName")
+ str(annee_scolaire) + f"{annee_scolaire}-{periode}-"
+ "-%s-" % periode
+ "-".join(etapes_apo) + "-".join(etapes_apo)
) )
basename = scu.unescape_html(basename) basename = scu.unescape_html(basename)

View File

@ -174,7 +174,7 @@ class DataEtudiant(object):
return self.data_apogee["nom"] + self.data_apogee["prenom"] return self.data_apogee["nom"] + self.data_apogee["prenom"]
def help(): 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"> <span>Explications sur les tableaux des effectifs et liste des
étudiants</span> étudiants</span>
@ -501,7 +501,7 @@ class EtapeBilan:
entete_liste_etudiant(), entete_liste_etudiant(),
self.table_effectifs(), self.table_effectifs(),
"""</details>""", """</details>""",
help(), _help(),
] ]
return "\n".join(H) return "\n".join(H)

View File

@ -29,6 +29,7 @@
""" """
from flask_login import current_user from flask_login import current_user
# --- Exceptions # --- Exceptions
class ScoException(Exception): class ScoException(Exception):
"super classe de toutes les exceptions ScoDoc." "super classe de toutes les exceptions ScoDoc."
@ -44,6 +45,7 @@ class ScoInvalidCSRF(ScoException):
class ScoValueError(ScoException): class ScoValueError(ScoException):
"Exception avec page d'erreur utilisateur, et qui stoque dest_url" "Exception avec page d'erreur utilisateur, et qui stoque dest_url"
# mal nommée: super classe de toutes les exceptions avec page # mal nommée: super classe de toutes les exceptions avec page
# d'erreur gentille. # d'erreur gentille.
def __init__(self, msg, dest_url=None): def __init__(self, msg, dest_url=None):
@ -75,7 +77,11 @@ class InvalidEtudId(NoteProcessError):
class ScoFormatError(ScoValueError): class ScoFormatError(ScoValueError):
pass "Erreur lecture d'un fichier fourni par l'utilisateur"
def __init__(self, msg, filename="", dest_url=None):
super().__init__(msg, dest_url=dest_url)
self.filename = filename
class ScoInvalidParamError(ScoValueError): class ScoInvalidParamError(ScoValueError):

View File

@ -84,7 +84,9 @@ class SemSet(dict):
self.semset_id = semset_id self.semset_id = semset_id
self["semset_id"] = semset_id self["semset_id"] = semset_id
self.sems = [] self.sems = []
self.formsemestre_ids = [] self.formsemestres = [] # modernisation en cours...
self.is_apc = False
self.formsemestre_ids = set()
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
if semset_id: # read existing set if semset_id: # read existing set
semsets = semset_list(cnx, args={"semset_id": semset_id}) semsets = semset_list(cnx, args={"semset_id": semset_id})
@ -123,8 +125,13 @@ class SemSet(dict):
def load_sems(self): def load_sems(self):
"""Load formsemestres""" """Load formsemestres"""
self.sems = [] self.sems = []
self.formsemestres = []
for formsemestre_id in self.formsemestre_ids: for formsemestre_id in self.formsemestre_ids:
self.sems.append(sco_formsemestre.get_formsemestre(formsemestre_id)) formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
self.formsemestres.append(formsemestre)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
self.sems.append(sem)
self["is_apc"] = formsemestre.formation.is_apc()
if self.sems: if self.sems:
self["date_debut"] = min([sem["date_debut_iso"] for sem in self.sems]) self["date_debut"] = min([sem["date_debut_iso"] for sem in self.sems])
@ -140,10 +147,10 @@ class SemSet(dict):
self["semlinks"] = [ self["semlinks"] = [
f"""<a class="stdlink" href="{ f"""<a class="stdlink" href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
formsemestre_id=sem["formsemestre_id"]) formsemestre_id=formsemestre.id)
}">{sem["titreannee"]}</a> }">{formsemestre.titre_annee()}</a>
""" """
for sem in self.sems for formsemestre in self.formsemestres
] ]
self["semtitles_str"] = "<br>".join(self["semlinks"]) self["semtitles_str"] = "<br>".join(self["semlinks"])
@ -168,18 +175,16 @@ class SemSet(dict):
f"can't add {formsemestre_id} to set {self.semset_id}: incompatible sem_id" f"can't add {formsemestre_id} to set {self.semset_id}: incompatible sem_id"
) )
if self.formsemestre_ids: if self.formsemestre_ids and formsemestre.formation.is_apc() != self["is_apc"]:
formsemestre_1 = formsemestre.query.get(self.formsemestre_ids[0]) raise ScoValueError(
if formsemestre.formation.is_apc() != formsemestre_1.formation.is_apc(): """On ne peut pas mélanger des semestres BUT/APC
raise ScoValueError( avec des semestres ordinaires dans le même export.""",
"""On ne peut pas mélanger des semestres BUT/APC dest_url=url_for(
avec des semestres ordinaires dans le même export.""", "notes.apo_semset_maq_status",
dest_url=url_for( scodoc_dept=g.scodoc_dept,
"notes.apo_semset_maq_status", semset_id=self.semset_id,
scodoc_dept=g.scodoc_dept, ),
semset_id=self.semset_id, )
),
)
ndb.SimpleQuery( ndb.SimpleQuery(
"""INSERT INTO notes_semset_formsemestre """INSERT INTO notes_semset_formsemestre

View File

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

View File

@ -0,0 +1,93 @@
XX-APO_TITRES-XX
apoC_annee 2021/2022
apoC_cod_dip VBTRET
apoC_Cod_Exp 2
apoC_cod_vdi 17
apoC_Fichier_Exp C:\TEMP\VBTRET-V1RET-V1RETW2.TXT
apoC_lib_dip BUT R et T
apoC_Titre1 Export Apogée du 17/04/2023 à 13:23
apoC_Titre2
XX-APO_TYP_RES-XX
10 AB1 AB2 ABI ABJ ADM AJ AJRO C1 DEF DIF
18 AB1 AB2 ABI ABJ ADM ADMC ADMD AJ AJAC AJAR AJRO ATT B1 C1 COMP DEF DIF NAR
45 ABI ABJ ADAC ADM ADMC ADMD AIR AJ AJAR AJCP AJRO AJS ATT B1 B2 C1 COMP CRED DEF DES DETT DIF ENER ENRA EXC INFA INFO INST LC MACS N1 N2 NAR NON NSUI NVAL OUI SUIV SUPS TELE TOEF TOIE VAL VALC VALR
10 ABI ABJ ADMC COMP DEF DIS NVAL VAL VALC VALR
AB1 : Ajourné en B2 mais admis en B1 AB2 : ADMIS en B1 mais ajourné en B2 ABI : Absence ABJ : Absence justifiée ADM : Admis AJ : Ajourné AJRO : Ajourné - Réorientation Obligatoire C1 : Niveau C1 DEF : Défaillant DIF : Décision différée
AB1 : Ajourné en B2 mais admis en B1 AB2 : ADMIS en B1 mais ajourné en B2 ABI : Absence ABJ : Absence justifiée ADM : Admis ADMC : Admis avec compensation ADMD : Admis (passage avec dette) AJ : Ajourné AJAC : Ajourné mais accès autorisé à étape sup. AJAR : Ajourné et Admis A Redoubler AJRO : Ajourné - Réorientation Obligatoire ATT : En attente de décison B1 : Niveau B1 C1 : Niveau C1 COMP : Compensé DEF : Défaillant DIF : Décision différée NAR : Ajourné non admis à redoubler
ABI : Absence ABJ : Absence justifiée ADAC : Admis avant choix ADM : Admis ADMC : Admis avec compensation ADMD : Admis (passage avec dette) AIR : Ingénieur spécialité Informatique appr AJ : Ajourné AJAR : Ajourné et Admis A Redoubler AJCP : Ajourné mais autorisé à compenser AJRO : Ajourné - Réorientation Obligatoire AJS : Ajourné (note éliminatoire) ATT : En attente de décison B1 : Niveau B1 B2 : Niveau B2 C1 : Niveau C1 COMP : Compensé CRED : Eléments en crédits DEF : Défaillant DES : Désistement DETT : Eléments en dettes DIF : Décision différée ENER : Ingénieur spécialité Energétique ENRA : Ingénieur spécialité Energétique appr EXC : Exclu INFA : Ingénieur spécialité Informatique appr INFO : Ingénieur spécialié Informatique INST : Ingénieur spécialité Instrumentation LC : Liste complémentaire MACS : Ingénieur spécialité MACS N1 : Compétences CLES N2 : Niveau N2 NAR : Ajourné non admis à redoubler NON : Non NSUI : Non suivi(e) NVAL : Non Validé(e) OUI : Oui SUIV : Suivi(e) SUPS : Supérieur au seuil TELE : Ingénieur spéciailté Télécommunications TOEF : TOEFL TOIE : TOEIC VAL : Validé(e) VALC : Validé(e) par compensation VALR : Validé(e) Retrospectivement
ABI : Absence ABJ : Absence justifiée ADMC : Admis avec compensation COMP : Compensé DEF : Défaillant DIS : Dispense examen NVAL : Non Validé(e) VAL : Validé(e) VALC : Validé(e) par compensation VALR : Validé(e) Retrospectivement
XX-APO_COLONNES-XX
apoL_a01_code Type Objet Code Version Année Session Admission/Admissibilité Type Rés. Etudiant Numéro
apoL_a02_nom Nom
apoL_a03_prenom Prénom
apoL_a04_naissance Session Admissibilité Naissance
APO_COL_VAL_DEB
apoL_c0001 ELP V1RETU21 2021 0 1 N V1RETU21 - UE 2.1 Administrer réseau 0 1 Note
apoL_c0002 ELP V1RETU21 2021 0 1 B 0 1 Barème
apoL_c0003 ELP V1RETU21 2021 0 1 J 0 1 Pts Jury
apoL_c0004 ELP V1RETU21 2021 0 1 R 0 1 Résultat
apoL_c0005 ELP V1RETU22 2021 0 1 N V1RETU22 - UE 2.2 Connecter entrep. 0 1 Note
apoL_c0006 ELP V1RETU22 2021 0 1 B 0 1 Barème
apoL_c0007 ELP V1RETU22 2021 0 1 J 0 1 Pts Jury
apoL_c0008 ELP V1RETU22 2021 0 1 R 0 1 Résultat
apoL_c0009 ELP V1RETU23 2021 0 1 N V1RETU23 - UE 2.3 Créer des outils 0 1 Note
apoL_c0010 ELP V1RETU23 2021 0 1 B 0 1 Barème
apoL_c0011 ELP V1RETU23 2021 0 1 J 0 1 Pts Jury
apoL_c0012 ELP V1RETU23 2021 0 1 R 0 1 Résultat
apoL_c0013 ELP VRETR201 2021 0 1 N VRETR201 - Technologie internet 0 1 Note
apoL_c0014 ELP VRETR201 2021 0 1 B 0 1 Barème
apoL_c0015 ELP VRETR202 2021 0 1 N VRETR202 - Administration système 0 1 Note
apoL_c0016 ELP VRETR202 2021 0 1 B 0 1 Barème
apoL_c0017 ELP VRETR203 2021 0 1 N VRETR203 - Bases service réseaux 0 1 Note
apoL_c0018 ELP VRETR203 2021 0 1 B 0 1 Barème
apoL_c0019 ELP VRETR204 2021 0 1 N VRETR204 - Initiation téléphonie 0 1 Note
apoL_c0020 ELP VRETR204 2021 0 1 B 0 1 Barème
apoL_c0021 ELP VRETR205 2021 0 1 N VRETR205 - Signaux et systèmes 0 1 Note
apoL_c0022 ELP VRETR205 2021 0 1 B 0 1 Barème
apoL_c0023 ELP VRETR206 2021 0 1 N VRETR206 - Numérisation information 0 1 Note
apoL_c0024 ELP VRETR206 2021 0 1 B 0 1 Barème
apoL_c0025 ELP VRETR207 2021 0 1 N VRETR207 - Sources de données 0 1 Note
apoL_c0026 ELP VRETR207 2021 0 1 B 0 1 Barème
apoL_c0027 ELP VRETR208 2021 0 1 N VRETR208 - Analyse et traitement 0 1 Note
apoL_c0028 ELP VRETR208 2021 0 1 B 0 1 Barème
apoL_c0029 ELP VRETR209 2021 0 1 N VRETR209 - Initiation au dév. 0 1 Note
apoL_c0030 ELP VRETR209 2021 0 1 B 0 1 Barème
apoL_c0031 ELP VRETR210 2021 0 1 N VRETR210 - Anglais de communication 0 1 Note
apoL_c0032 ELP VRETR210 2021 0 1 B 0 1 Barème
apoL_c0033 ELP VRETR211 2021 0 1 N VRETR211 - Exp., culture, com. pro2 0 1 Note
apoL_c0034 ELP VRETR211 2021 0 1 B 0 1 Barème
apoL_c0035 ELP VRETR212 2021 0 1 N VRETR212 - PPP 0 1 Note
apoL_c0036 ELP VRETR212 2021 0 1 B 0 1 Barème
apoL_c0037 ELP VRETR213 2021 0 1 N VRETR213 - Maths des syst. num 0 1 Note
apoL_c0038 ELP VRETR213 2021 0 1 B 0 1 Barème
apoL_c0039 ELP VRETR214 2021 0 1 N VRETR214 - Analyse mathématique 0 1 Note
apoL_c0040 ELP VRETR214 2021 0 1 B 0 1 Barème
apoL_c0041 ELP VRETS21 2021 0 1 N VRETS21 - Construire un réseau 0 1 Note
apoL_c0042 ELP VRETS21 2021 0 1 B 0 1 Barème
apoL_c0043 ELP VRETS22 2021 0 1 N VRETS22 - Mesurer et car. un signal 0 1 Note
apoL_c0044 ELP VRETS22 2021 0 1 B 0 1 Barème
apoL_c0045 ELP VRETS23 2021 0 1 N VRETS23 - Mettre en place une solut 0 1 Note
apoL_c0046 ELP VRETS23 2021 0 1 B 0 1 Barème
apoL_c0047 ELP VRETS24 2021 0 1 N VRETS24 - Projet intégratif 0 1 Note
apoL_c0048 ELP VRETS24 2021 0 1 B 0 1 Barème
apoL_c0049 ELP VRETS25 2021 0 1 N VRETS25 - Portfolio 0 1 Note
apoL_c0050 ELP VRETS25 2021 0 1 B 0 1 Barème
apoL_c0051 ELP V1RETW2 2021 0 1 N V1RETW2 - Semestre 2 BUT RT 2 0 1 Note
apoL_c0052 ELP V1RETW2 2021 0 1 B 0 1 Barème
apoL_c0053 ELP V1RETW2 2021 0 1 J 0 1 Pts Jury
apoL_c0054 ELP V1RETW2 2021 0 1 R 0 1 Résultat
apoL_c0055 ELP V1RETO 2021 0 1 N V1RETO - Année BUT 1 RT 0 1 Note
apoL_c0056 ELP V1RETO 2021 0 1 B 0 1 Barème
apoL_c0057 VET V1RET 117 2021 0 1 N V1RET - BUT RT an1 0 1 Note
apoL_c0058 VET V1RET 117 2021 0 1 B 0 1 Barème
apoL_c0059 VET V1RET 117 2021 0 1 J 0 1 Pts Jury
apoL_c0060 VET V1RET 117 2021 0 1 R 0 1 Résultat
APO_COL_VAL_FIN
apoL_c0061 APO_COL_VAL_FIN
XX-APO_VALEURS-XX
apoL_a01_code apoL_a02_nom apoL_a03_prenom apoL_a04_naissance apoL_c0001 apoL_c0002 apoL_c0003 apoL_c0004 apoL_c0005 apoL_c0006 apoL_c0007 apoL_c0008 apoL_c0009 apoL_c0010 apoL_c0011 apoL_c0012 apoL_c0013 apoL_c0014 apoL_c0015 apoL_c0016 apoL_c0017 apoL_c0018 apoL_c0019 apoL_c0020 apoL_c0021 apoL_c0022 apoL_c0023 apoL_c0024 apoL_c0025 apoL_c0026 apoL_c0027 apoL_c0028 apoL_c0029 apoL_c0030 apoL_c0031 apoL_c0032 apoL_c0033 apoL_c0034 apoL_c0035 apoL_c0036 apoL_c0037 apoL_c0038 apoL_c0039 apoL_c0040 apoL_c0041 apoL_c0042 apoL_c0043 apoL_c0044 apoL_c0045 apoL_c0046 apoL_c0047 apoL_c0048 apoL_c0049 apoL_c0050 apoL_c0051 apoL_c0052 apoL_c0053 apoL_c0054 apoL_c0055 apoL_c0056 apoL_c0057 apoL_c0058 apoL_c0059 apoL_c0060
12345678 INCONNU ETUDIANT 10/01/2003 ADM
22345678 UN CONNU ETUDIANT 10/07/2003 AJ

View File

@ -0,0 +1,67 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
""" Test lecture/érciture fichiers Apogée
"""
import pytest
from flask import g
import app
from app import db
from app.models import Formation, FormSemestreEtape
from app.scodoc import sco_apogee_csv, sco_apogee_reader
from config import TestConfig
from tests.conftest import RESOURCES_DIR
from tests.unit import yaml_setup
DEPT = TestConfig.DEPT_TEST
APO_CSV_FILE = RESOURCES_DIR + "/apogee/V1RET!117.txt"
def test_apogee_csv(test_client):
"""Lecture/écriture d'un fichier Apogée: vérifie que le fichier écrit est
strictement identique au fichier lu.
(le semestre n'ayant aucun résultat)
"""
app.set_sco_dept(DEPT)
# Met en place une formation et un semestre
formation = Formation(
dept_id=g.scodoc_dept_id,
acronyme="TESTAPO",
titre="Test Apo",
titre_officiel="Test Apof",
)
db.session.add(formation)
formsemestre = yaml_setup.create_formsemestre(
formation, [], 1, "S1_apo", "2021-09-01", "2022-01-15"
)
etape = FormSemestreEtape(etape_apo="V1RET!117")
formsemestre.etapes.append(etape)
db.session.add(formsemestre)
db.session.commit()
#
with open(APO_CSV_FILE, encoding=sco_apogee_reader.APO_INPUT_ENCODING) as f:
data = f.read()
assert "ETUDIANT" in data
#
apo_data = sco_apogee_csv.ApoData(data, periode=2)
apo_data.setup()
assert len(apo_data.apo_csv.csv_etuds) == 2
apo_etuds = apo_data.etud_by_nip.values()
for apo_etud in apo_etuds:
apo_etud.is_apc = apo_data.is_apc
apo_etud.lookup_scodoc(apo_data.etape_formsemestre_ids)
apo_etud.associate_sco(apo_data)
data_2 = (
apo_data.apo_csv.write(apo_etuds)
.decode(sco_apogee_reader.APO_INPUT_ENCODING)
.replace("\r", "")
)
# open("toto.txt", "w", encoding=sco_apogee_reader.APO_INPUT_ENCODING).write(data_2)
assert data == data_2