# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # 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 # ############################################################################## """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 """