From 0f2579dc0f846b5d3bca31c5d2ffacda2aa5e394 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 7 Sep 2023 23:09:39 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Fix:=20gestion=20des=20archives=20(confusio?= =?UTF-8?q?n=20de=20d=C3=A9partements)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/justificatifs.py | 30 ++--- app/scodoc/sco_archives.py | 141 +++++++++++++++-------- app/scodoc/sco_archives_etud.py | 54 +++++---- app/scodoc/sco_archives_justificatifs.py | 84 +++++++------- app/scodoc/sco_etape_apogee.py | 18 +-- app/scodoc/sco_formsemestre_status.py | 2 +- app/scodoc/sco_page_etud.py | 2 +- app/views/notes.py | 2 +- sco_version.py | 2 +- 9 files changed, 193 insertions(+), 142 deletions(-) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 0fd59b97..5f66fa60 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -8,8 +8,9 @@ from datetime import datetime from flask_json import as_json -from flask import g, jsonify, request +from flask import g, request from flask_login import login_required, current_user +from flask_sqlalchemy.query import Query import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu @@ -26,7 +27,6 @@ from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error -from flask_sqlalchemy.query import Query # Partie Modèle @@ -436,7 +436,7 @@ def _delete_singular(justif_id: int, database): if archive_name is not None: archiver: JustificatifArchiver = JustificatifArchiver() try: - archiver.delete_justificatif(justificatif_unique.etudid, archive_name) + archiver.delete_justificatif(justificatif_unique.etudiant, archive_name) except ValueError: pass @@ -481,7 +481,7 @@ def justif_import(justif_id: int = None): try: fname: str archive_name, fname = archiver.save_justificatif( - etudid=justificatif_unique.etudid, + justificatif_unique.etudiant, filename=file.filename, data=file.stream.read(), archive_name=archive_name, @@ -512,7 +512,7 @@ def justif_export(justif_id: int = None, filename: str = None): if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificaitf = query.first_or_404() + justificatif_unique: Justificatif = query.first_or_404() archive_name: str = justificatif_unique.fichier if archive_name is None: @@ -522,7 +522,7 @@ def justif_export(justif_id: int = None, filename: str = None): try: return archiver.get_justificatif_file( - archive_name, justificatif_unique.etudid, filename + archive_name, justificatif_unique.etudiant, filename ) except ScoValueError as err: return json_error(404, err.args[0]) @@ -564,10 +564,10 @@ def justif_remove(justif_id: int = None): if remove is None or remove not in ("all", "list"): return json_error(404, "param 'remove': Valeur invalide") archiver: JustificatifArchiver = JustificatifArchiver() - etudid: int = justificatif_unique.etudid + etud = justificatif_unique.etudiant try: if remove == "all": - archiver.delete_justificatif(etudid=etudid, archive_name=archive_name) + archiver.delete_justificatif(etud, archive_name=archive_name) justificatif_unique.fichier = None db.session.add(justificatif_unique) db.session.commit() @@ -575,13 +575,13 @@ def justif_remove(justif_id: int = None): else: for fname in data.get("filenames", []): archiver.delete_justificatif( - etudid=etudid, + etud, archive_name=archive_name, filename=fname, ) - if len(archiver.list_justificatifs(archive_name, etudid)) == 0: - archiver.delete_justificatif(etudid, archive_name) + if len(archiver.list_justificatifs(archive_name, etud)) == 0: + archiver.delete_justificatif(etud, archive_name) justificatif_unique.fichier = None db.session.add(justificatif_unique) db.session.commit() @@ -616,16 +616,16 @@ def justif_list(justif_id: int = None): archiver: JustificatifArchiver = JustificatifArchiver() if archive_name is not None: filenames = archiver.list_justificatifs( - archive_name, justificatif_unique.etudid + archive_name, justificatif_unique.etudiant ) retour = {"total": len(filenames), "filenames": []} - for fi in filenames: - if int(fi[1]) == current_user.id or current_user.has_permission( + for filename in filenames: + if int(filename[1]) == current_user.id or current_user.has_permission( Permission.ScoJustifView ): - retour["filenames"].append(fi[0]) + retour["filenames"].append(filename[0]) return retour diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index c6a646ee..51bba4b2 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -81,7 +81,7 @@ from app.scodoc import sco_pv_pdf from app.scodoc.sco_exceptions import ScoValueError -class BaseArchiver(object): +class BaseArchiver: def __init__(self, archive_type=""): self.archive_type = archive_type self.initialized = False @@ -92,14 +92,17 @@ class BaseArchiver(object): "set dept" self.dept_id = dept_id - def initialize(self): + def initialize(self, dept_id: int = None): + """Fixe le département et initialise les répertoires au besoin.""" + # Set departement (à chaque fois car peut changer d'une utilisation à l'autre) + self.dept_id = getattr(g, "scodoc_dept_id") if dept_id is None else dept_id if self.initialized: return dirs = [Config.SCODOC_VAR_DIR, "archives"] if self.archive_type: dirs.append(self.archive_type) - self.root = os.path.join(*dirs) + self.root = os.path.join(*dirs) # /opt/scodoc-data/archives/ log("initialized archiver, path=" + self.root) path = dirs[0] for directory in dirs[1:]: @@ -112,15 +115,13 @@ class BaseArchiver(object): finally: scu.GSL.release() self.initialized = True - if self.dept_id is None: - self.dept_id = getattr(g, "scodoc_dept_id") - def get_obj_dir(self, oid: int): + def get_obj_dir(self, oid: int, dept_id: int = None): """ :return: path to directory of archives for this object (eg formsemestre_id or etudid). If directory does not yet exist, create it. """ - self.initialize() + self.initialize(dept_id) dept_dir = os.path.join(self.root, str(self.dept_id)) try: scu.GSL.acquire() @@ -141,21 +142,21 @@ class BaseArchiver(object): scu.GSL.release() return obj_dir - def list_oids(self): + def list_oids(self, dept_id: int = None): """ :return: list of archive oids """ - self.initialize() + self.initialize(dept_id) base = os.path.join(self.root, str(self.dept_id)) + os.path.sep dirs = glob.glob(base + "*") return [os.path.split(x)[1] for x in dirs] - def list_obj_archives(self, oid: int): + def list_obj_archives(self, oid: int, dept_id: int = None): """Returns :return: list of archive identifiers for this object (paths to non empty dirs) """ - self.initialize() - base = self.get_obj_dir(oid) + os.path.sep + self.initialize(dept_id) + base = self.get_obj_dir(oid, dept_id=dept_id) + os.path.sep dirs = glob.glob( base + "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]" @@ -165,9 +166,9 @@ class BaseArchiver(object): dirs.sort() return dirs - def delete_archive(self, archive_id: str): + def delete_archive(self, archive_id: str, dept_id: int = None): """Delete (forever) this archive""" - self.initialize() + self.initialize(dept_id) try: scu.GSL.acquire() shutil.rmtree(archive_id, ignore_errors=True) @@ -180,9 +181,9 @@ class BaseArchiver(object): *[int(x) for x in os.path.split(archive_id)[1].split("-")] ) - def list_archive(self, archive_id: str) -> str: + def list_archive(self, archive_id: str, dept_id: int = None) -> str: """Return list of filenames (without path) in archive""" - self.initialize() + self.initialize(dept_id) try: scu.GSL.acquire() files = os.listdir(archive_id) @@ -201,12 +202,12 @@ class BaseArchiver(object): "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name ) - def get_id_from_name(self, oid, archive_name: str): + def get_id_from_name(self, oid, archive_name: str, dept_id: int = None): """returns archive id (check that name is valid)""" - self.initialize() + self.initialize(dept_id) if not self.is_valid_archive_name(archive_name): raise ScoValueError(f"Archive {archive_name} introuvable") - archive_id = os.path.join(self.get_obj_dir(oid), archive_name) + archive_id = os.path.join(self.get_obj_dir(oid, dept_id=dept_id), archive_name) if not os.path.isdir(archive_id): log( f"invalid archive name: {archive_name}, oid={oid}, archive_id={archive_id}" @@ -214,9 +215,9 @@ class BaseArchiver(object): raise ScoValueError(f"Archive {archive_name} introuvable") return archive_id - def get_archive_description(self, archive_id: str) -> str: + def get_archive_description(self, archive_id: str, dept_id: int = None) -> str: """Return description of archive""" - self.initialize() + self.initialize(dept_id) filename = os.path.join(archive_id, "_description.txt") try: with open(filename, encoding=scu.SCO_ENCODING) as f: @@ -229,11 +230,11 @@ class BaseArchiver(object): return descr - def create_obj_archive(self, oid: int, description: str): + def create_obj_archive(self, oid: int, description: str, dept_id: int = None): """Creates a new archive for this object and returns its id.""" # id suffixé par YYYY-MM-DD-hh-mm-ss archive_id = ( - self.get_obj_dir(oid) + self.get_obj_dir(oid, dept_id=dept_id) + os.path.sep + "-".join([f"{x:02d}" for x in time.localtime()[:6]]) ) @@ -248,7 +249,13 @@ class BaseArchiver(object): self.store(archive_id, "_description.txt", description) return archive_id - def store(self, archive_id: str, filename: str, data: Union[str, bytes]): + def store( + self, + archive_id: str, + filename: str, + data: Union[str, bytes], + dept_id: int = None, + ): """Store data in archive, under given filename. Filename may be modified (sanitized): return used filename The file is created or replaced. @@ -256,7 +263,7 @@ class BaseArchiver(object): """ if isinstance(data, str): data = data.encode(scu.SCO_ENCODING) - self.initialize() + self.initialize(dept_id) filename = scu.sanitize_filename(filename) log(f"storing {filename} ({len(data)} bytes) in {archive_id}") try: @@ -268,9 +275,9 @@ class BaseArchiver(object): scu.GSL.release() return filename - def get(self, archive_id: str, filename: str): + def get(self, archive_id: str, filename: str, dept_id: int = None): """Retreive data""" - self.initialize() + self.initialize(dept_id) if not scu.is_valid_filename(filename): log(f"""Archiver.get: invalid filename '{filename}'""") raise ScoValueError("archive introuvable (déjà supprimée ?)") @@ -280,11 +287,11 @@ class BaseArchiver(object): data = f.read() return data - def get_archived_file(self, oid, archive_name, filename): + def get_archived_file(self, oid, archive_name, filename, dept_id: int = None): """Recupère les donnees du fichier indiqué et envoie au client. Returns: Response """ - archive_id = self.get_id_from_name(oid, archive_name) + archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id) data = self.get(archive_id, filename) mime = mimetypes.guess_type(filename)[0] if mime is None: @@ -298,7 +305,7 @@ class SemsArchiver(BaseArchiver): BaseArchiver.__init__(self, archive_type="") -PVArchive = SemsArchiver() +PV_ARCHIVER = SemsArchiver() # ---------------------------------------------------------------------------- @@ -332,8 +339,10 @@ def do_formsemestre_archive( formsemestre = FormSemestre.get_formsemestre(formsemestre_id) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) sem_archive_id = formsemestre_id - archive_id = PVArchive.create_obj_archive(sem_archive_id, description) - date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") + archive_id = PV_ARCHIVER.create_obj_archive( + sem_archive_id, description, formsemestre.dept_id + ) + date = PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") if not group_ids: # tous les inscrits du semestre @@ -347,7 +356,12 @@ def do_formsemestre_archive( # Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes) data, _ = gen_formsemestre_recapcomplet_excel(res, include_evaluations=True) if data: - PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data) + PV_ARCHIVER.store( + archive_id, + "Tableau_moyennes" + scu.XLSX_SUFFIX, + data, + dept_id=formsemestre.dept_id, + ) # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes) table_html, _, _ = gen_formsemestre_recapcomplet_html_table( formsemestre, res, include_evaluations=True @@ -367,13 +381,17 @@ def do_formsemestre_archive( html_sco_header.sco_footer(), ] ) - PVArchive.store(archive_id, "Tableau_moyennes.html", data) + PV_ARCHIVER.store( + archive_id, "Tableau_moyennes.html", data, dept_id=formsemestre.dept_id + ) # Bulletins en JSON data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True) data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) if data: - PVArchive.store(archive_id, "Bulletins.json", data_js) + PV_ARCHIVER.store( + archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id + ) # Décisions de jury, en XLS if formsemestre.formation.is_apc(): response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls") @@ -383,17 +401,23 @@ def do_formsemestre_archive( formsemestre_id, format="xls", publish=False ) if data: - PVArchive.store( + PV_ARCHIVER.store( archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data, + dept_id=formsemestre.dept_id, ) # Classeur bulletins (PDF) data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( formsemestre_id, version=bul_version ) if data: - PVArchive.store(archive_id, "Bulletins.pdf", data) + PV_ARCHIVER.store( + archive_id, + "Bulletins.pdf", + data, + dept_id=formsemestre.dept_id, + ) # Lettres individuelles (PDF): data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles( formsemestre_id, @@ -403,7 +427,12 @@ def do_formsemestre_archive( signature=signature, ) if data: - PVArchive.store(archive_id, f"CourriersDecisions{groups_filename}.pdf", data) + PV_ARCHIVER.store( + archive_id, + f"CourriersDecisions{groups_filename}.pdf", + data, + dept_id=formsemestre.dept_id, + ) # PV de jury (PDF): data = sco_pv_pdf.pvjury_pdf( @@ -419,7 +448,12 @@ def do_formsemestre_archive( anonymous=anonymous, ) if data: - PVArchive.store(archive_id, f"PV_Jury{groups_filename}.pdf", data) + PV_ARCHIVER.store( + archive_id, + f"PV_Jury{groups_filename}.pdf", + data, + dept_id=formsemestre.dept_id, + ) def formsemestre_archive(formsemestre_id, group_ids: list[int] = None): @@ -558,14 +592,21 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement. def formsemestre_list_archives(formsemestre_id): """Page listing archives""" + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) sem_archive_id = formsemestre_id L = [] - for archive_id in PVArchive.list_obj_archives(sem_archive_id): + for archive_id in PV_ARCHIVER.list_obj_archives( + sem_archive_id, dept_id=formsemestre.dept_id + ): a = { "archive_id": archive_id, - "description": PVArchive.get_archive_description(archive_id), - "date": PVArchive.get_archive_date(archive_id), - "content": PVArchive.list_archive(archive_id), + "description": PV_ARCHIVER.get_archive_description( + archive_id, dept_id=formsemestre.dept_id + ), + "date": PV_ARCHIVER.get_archive_date(archive_id), + "content": PV_ARCHIVER.list_archive( + archive_id, dept_id=formsemestre.dept_id + ), } L.append(a) @@ -575,7 +616,7 @@ def formsemestre_list_archives(formsemestre_id): else: H.append("