ScoDoc-PE/app/scodoc/sco_etape_apogee.py

466 lines
15 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 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
#
##############################################################################
"""ScoDoc : stockage et vérifications des "maquettes" Apogée
(fichiers CSV pour l'export vers Apogée)
associées aux années scolaires
Voir sco_apogee_csv.py pour la structure du fichier Apogée.
Stockage: utilise sco_archive.py
exemple:
/opt/scodoc-data/archives/apo_csv/<dept_id>/2016-1/2016-07-03-16-12-19/V3ASR!111.csv
pour une maquette de l'étape V3ASR version VDI 111.
La version VDI sera ignorée sauf si elle est indiquée dans l'étape du semestre.
apo_csv_get()
API:
# apo_csv_store(csv_data, annee_scolaire, sem_id)
store maq file (archive)
apo_csv_get(etape_apo, annee_scolaire, sem_id, vdi_apo=None)
get maq data (read stored file and returns string)
if vdi_apo, get maq for this etape/vdi, else returns the first matching etape.
apo_csv_delete(etape_apo, annee_scolaire, sem_id)
apo_csv_list_stored_etapes(annee_scolaire=None, sem_id=None, etapes=None)
returns: liste des codes etapes et version vdi stockés (pour l'annee_scolaire et le sem_id indiqués)
apo_csv_semset_check(semset)
check students in stored maqs vs students in sem
Cas à détecter:
- etudiants ScoDoc sans code NIP
- etudiants dans sem (ScoDoc) mais dans aucun CSV
- etudiants dans un CSV mais pas dans sem ScoDoc
- etudiants dans plusieurs CSV (argh!)
detecte aussi si on a plusieurs années scolaires
returns: etuds_ok (in ScoDoc and CSVs)
etuds_no_apo
unknown_apo : liste de { 'NIP', 'nom', 'prenom' }
dups_apo : liste de { 'NIP', 'nom', 'prenom', 'etapes_apo' }
etapes_missing_csv : liste des étapes du semestre sans maquette CSV
apo_csv_check_etape(semset, set_nips, etape_apo)
check une etape
"""
import re
import app.scodoc.sco_utils as scu
from app.scodoc import sco_archives
from app.scodoc import sco_apogee_csv, sco_apogee_reader
from app.scodoc.sco_exceptions import ScoValueError
class ApoCSVArchiver(sco_archives.BaseArchiver):
def __init__(self):
sco_archives.BaseArchiver.__init__(self, archive_type="apo_csv")
APO_CSV_ARCHIVER = ApoCSVArchiver()
# def get_sem_apo_archive(formsemestre_id):
# """Get, or create if necessary, the archive for apo CSV files"""
# archive_id
# return archive_id
def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
"""
csv_data: maquette content (str))
annee_scolaire: int (2016)
sem_id: 0 (année ?), 1 (premier semestre de l'année) ou 2 (deuxième semestre)
:return: etape_apo du fichier CSV stocké
Note: le fichier CSV est stocké encodé en APO_OUTPUT_ENCODING
"""
# sanity check
filesize = len(csv_data)
if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE:
raise ScoValueError(f"Fichier csv de taille invalide ! ({filesize})")
if not annee_scolaire:
raise ScoValueError("Impossible de déterminer l'année scolaire !")
apo_data = sco_apogee_csv.ApoData(
csv_data, periode=sem_id
) # parse le fichier -> exceptions
filename = str(apo_data.etape) + ".csv" # will concatenate VDI to etape
if str(apo_data.etape) in apo_csv_list_stored_etapes(annee_scolaire, sem_id=sem_id):
raise ScoValueError(
f"Etape {apo_data.etape} déjà stockée pour cette année scolaire !"
)
oid = f"{annee_scolaire}-{sem_id}"
description = f"""{str(apo_data.etape)};{annee_scolaire};{sem_id}"""
archive_id = APO_CSV_ARCHIVER.create_obj_archive(oid, description)
csv_data_bytes = csv_data.encode(sco_apogee_reader.APO_OUTPUT_ENCODING)
APO_CSV_ARCHIVER.store(archive_id, filename, csv_data_bytes)
return apo_data.etape
def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None):
"""
:return: list of informations about stored CSV
[ { } ]
"""
oids = APO_CSV_ARCHIVER.list_oids() # [ '2016-1', ... ]
# filter
if annee_scolaire:
e = re.compile(str(annee_scolaire) + "-.+")
oids = [x for x in oids if e.match(x)]
if sem_id:
e = re.compile(r"[0-9]{4}-" + str(sem_id))
oids = [x for x in oids if e.match(x)]
infos = [] # liste d'infos
for oid in oids:
archive_ids = APO_CSV_ARCHIVER.list_obj_archives(oid)
for archive_id in archive_ids:
description = APO_CSV_ARCHIVER.get_archive_description(archive_id)
fs = tuple(description.split(";"))
if len(fs) == 3:
arch_etape_apo, arch_annee_scolaire, arch_sem_id = fs
else:
raise ValueError("Archive invalide: " + archive_id)
if (etapes is None) or (arch_etape_apo in etapes):
infos.append(
{
"archive_id": archive_id,
"annee_scolaire": int(arch_annee_scolaire),
"sem_id": int(arch_sem_id),
"etape_apo": arch_etape_apo, # qui contient éventuellement le VDI
"date": APO_CSV_ARCHIVER.get_archive_date(archive_id),
}
)
infos.sort(key=lambda x: x["etape_apo"])
return infos
def apo_csv_list_stored_etapes(annee_scolaire, sem_id=None, etapes=None):
"""
:return: list of stored etapes [ ApoEtapeVDI, ... ]
"""
infos = apo_csv_list_stored_archives(
annee_scolaire=annee_scolaire, sem_id=sem_id, etapes=etapes
)
return [info["etape_apo"] for info in infos]
def apo_csv_delete(archive_id):
"""Delete archived CSV"""
APO_CSV_ARCHIVER.delete_archive(archive_id)
def apo_csv_get_archive(etape_apo, annee_scolaire="", sem_id=""):
"""Get archive"""
stored_archives = apo_csv_list_stored_archives(
annee_scolaire=annee_scolaire, sem_id=sem_id
)
for info in stored_archives:
if info["etape_apo"] == etape_apo:
return info
return None
def apo_csv_get(etape_apo="", annee_scolaire="", sem_id="") -> str:
"""Get CSV data for given etape_apo
:return: CSV, as a data string
"""
info = apo_csv_get_archive(etape_apo, annee_scolaire, sem_id)
if not info:
raise ScoValueError(
"Etape %s non enregistree (%s, %s)" % (etape_apo, annee_scolaire, sem_id)
)
archive_id = info["archive_id"]
data = APO_CSV_ARCHIVER.get(archive_id, etape_apo + ".csv")
# ce fichier a été archivé donc généré par ScoDoc
# son encodage est donc APO_OUTPUT_ENCODING
return data.decode(sco_apogee_reader.APO_OUTPUT_ENCODING)
# ------------------------------------------------------------------------
def apo_get_sem_etapes(sem):
"""Etapes de ce semestre: pour l'instant, celles déclarées
Dans une future version, on pourrait aussi utiliser les étapes
d'inscription des étudiants, recupérées via le portail,
voir check_paiement_etuds().
:return: list of etape_apo (ApoEtapeVDI instances)
"""
return sem["etapes"]
def apo_csv_check_etape(semset, set_nips, etape_apo):
"""Check etape vs set of sems"""
# Etudiants dans la maquette CSV:
csv_data = apo_csv_get(etape_apo, semset["annee_scolaire"], semset["sem_id"])
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
apo_nips = {e["nip"] for e in apo_data.etuds}
#
nips_ok = set_nips.intersection(apo_nips)
nips_no_apo = set_nips - apo_nips # dans ScoDoc mais pas dans cette maquette Apogée
nips_no_sco = apo_nips - set_nips # dans Apogée mais pas dans ScoDoc
# Elements Apogee vs ScoDoc
apo_data.setup()
maq_elems, sem_elems = apo_data.list_elements()
return nips_ok, apo_nips, nips_no_apo, nips_no_sco, maq_elems, sem_elems
def apo_csv_semset_check(
semset, allow_missing_apo=False, allow_missing_csv=False
): # was apo_csv_check
"""
check students in stored maqs vs students in semset
Cas à détecter:
- étapes sans maquette CSV (etapes_missing_csv)
- etudiants ScoDoc sans code NIP (etuds_without_nip)
- etudiants dans semset (ScoDoc) mais dans aucun CSV (nips_no_apo)
- etudiants dans un CSV mais pas dans semset ScoDoc (nips_no_sco)
- etudiants dans plusieurs CSV (argh!)
+ si plusieurs annees scolaires
"""
# Etapes du semestre sans maquette CSV:
etapes_apo = apo_csv_list_stored_etapes(
semset["annee_scolaire"], semset["sem_id"], etapes=semset.list_etapes()
)
etapes_missing_csv = []
for e in semset.list_etapes():
if not e in etapes_apo:
etapes_missing_csv.append(e)
# Etudiants inscrits dans ce semset:
semset.load_etuds()
set_nips = set().union(*[s["nips"] for s in semset.sems])
#
nips_ok = set() # codes nip des etudiants dans ScoDoc et Apogée
nips_no_apo = set_nips.copy() # dans ScoDoc mais pas dans Apogée
nips_no_sco = set() # dans Apogée mais pas dans ScoDoc
etapes_apo_nips = [] # liste des nip de chaque maquette
maq_elems = set()
sem_elems = set()
for etape_apo in etapes_apo:
(
et_nips_ok,
et_apo_nips,
et_nips_no_apo,
et_nips_no_sco,
et_maq_elems,
et_sem_elems,
) = apo_csv_check_etape(semset, set_nips, etape_apo)
nips_ok |= et_nips_ok
nips_no_apo -= et_apo_nips
nips_no_sco |= et_nips_no_sco
etapes_apo_nips.append(et_apo_nips)
maq_elems |= et_maq_elems
sem_elems |= et_sem_elems
# doublons: etudiants mentionnés dans plusieurs maquettes Apogée:
apo_dups = set()
if len(etapes_apo_nips) > 1:
all_nips = etapes_apo_nips[0]
for etape_apo_nips in etapes_apo_nips[1:]:
apo_dups |= all_nips & etape_apo_nips
all_nips |= etape_apo_nips
# All ok ?
ok_for_export = (
((not etapes_missing_csv) or allow_missing_csv)
and (not semset["etuds_without_nip"])
and ((not nips_no_apo) or allow_missing_apo)
and (not apo_dups)
and len(semset.annees_scolaires()) <= 1
)
return (
ok_for_export,
etapes_missing_csv,
semset["etuds_without_nip"],
nips_ok,
nips_no_apo,
nips_no_sco,
apo_dups,
maq_elems,
sem_elems,
)
def apo_csv_retreive_etuds_by_nip(semset, nips):
"""
Search info about listed nips in stored CSV
:return: list [ { 'etape_apo', 'nip', 'nom', 'prenom' } ]
"""
apo_etuds_by_nips = {}
etapes_apo = apo_csv_list_stored_etapes(semset["annee_scolaire"], semset["sem_id"])
for etape_apo in etapes_apo:
csv_data = apo_csv_get(etape_apo, semset["annee_scolaire"], semset["sem_id"])
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
etape_apo = apo_data.etape_apogee
for e in apo_data.etuds:
e["etape_apo"] = etape_apo
apo_etuds_by_nips.update(dict([(e["nip"], e) for e in apo_data.etuds]))
etuds = {} # { nip : etud or None }
for nip in nips:
etuds[nip] = apo_etuds_by_nips.get(nip, {"nip": nip, "etape_apo": "?"})
return etuds
"""
Tests:
from debug import *
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_formsemestre
from app.scodoc.sco_etape_apogee import *
from app.scodoc.sco_apogee_csv import *
from app.scodoc.sco_semset import *
app.set_sco_dept('RT')
csv_data = open('/opt/misc/VDTRT_V1RT.TXT').read()
annee_scolaire=2015
sem_id=1
apo_data = sco_apogee_csv.ApoData(csv_data, periode=sem_id)
print apo_data.etape_apogee
apo_data.setup()
e = apo_data.etuds[0]
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
e.associate_sco( apo_data)
print apo_csv_list_stored_archives()
# apo_csv_store(csv_data, annee_scolaire, sem_id)
groups_infos = sco_groups_view.DisplayedGroupsInfos( [sco_groups.get_default_group(formsemestre_id)], formsemestre_id=formsemestre_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
#
s = SemSet('NSS29902')
apo_data = sco_apogee_csv.ApoData(open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2015-2/2016-07-10-11-26-15/V1RT.csv').read(), periode=1)
# cas Tiziri K. (inscrite en S1, démission en fin de S1, pas inscrite en S2)
# => pas de décision, ce qui est voulu (?)
#
apo_data.setup()
e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0]
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
e.associate_sco(apo_data)
self=e
col_id='apoL_c0129'
# --
from app.scodoc import sco_portal_apogee
_ = go_dept(app, 'GEA').Notes
#csv_data = sco_portal_apogee.get_maquette_apogee(etape='V1GE', annee_scolaire=2015)
csv_data = open('/tmp/V1GE.txt').read()
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
# ------
# les elements inconnus:
from debug import *
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_formsemestre
from app.scodoc.sco_etape_apogee import *
from app.scodoc.sco_apogee_csv import *
from app.scodoc.sco_semset import *
_ = go_dept(app, 'RT').Notes
csv_data = open('/opt/misc/V2RT.csv').read()
annee_scolaire=2015
sem_id=1
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
print apo_data.etape_apogee
apo_data.setup()
for e in apo_data.etuds:
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
e.associate_sco(apo_data)
# ------
# test export jury intermediaire
from debug import *
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_formsemestre
from app.scodoc.sco_etape_apogee import *
from app.scodoc.sco_apogee_csv import *
from app.scodoc.sco_semset import *
_ = go_dept(app, 'CJ').Notes
csv_data = open('/opt/scodoc/var/scodoc/archives/apo_csv/CJ/2016-1/2017-03-06-21-46-32/V1CJ.csv').read()
annee_scolaire=2016
sem_id=1
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
print apo_data.etape_apogee
apo_data.setup()
e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0] #
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
e.associate_sco(apo_data)
self=e
sco_elts = {}
col_id='apoL_c0001'
code = apo_data.cols[col_id]['Code'] # 'V1RT'
sem = apo_data.sems_periode[0] # le S1
"""