From 34aab0a46fa2ae8841ae9c16ac877820d999c4ae Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Wed, 26 Jun 2024 21:26:51 +0200
Subject: [PATCH] =?UTF-8?q?Element=20de=20passage=20dans=20apog=C3=A9e.=20?=
 =?UTF-8?q?Close=20#937?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/scodoc/sco_apogee_csv.py    | 39 ++++++++++++++++++++++++++++-----
 app/scodoc/sco_apogee_reader.py |  7 +++++-
 app/scodoc/sco_exceptions.py    | 27 ++++++++++++-----------
 3 files changed, 54 insertions(+), 19 deletions(-)

diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
index c56f497f0..f2dba69c9 100644
--- a/app/scodoc/sco_apogee_csv.py
+++ b/app/scodoc/sco_apogee_csv.py
@@ -221,7 +221,8 @@ class ApoEtud(dict):
                     except KeyError as exc:
                         raise ScoFormatError(
                             f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{
-                                col_id}</tt> non déclarée ?"""
+                                col_id}</tt> non déclarée ?""",
+                            safe=True,
                         ) from exc
                 else:
                     try:
@@ -326,9 +327,14 @@ class ApoEtud(dict):
                 self.log.append("export étape désactivé")
                 return VOID_APO_RES
 
+        # Element passage
+        res_passage = self.search_elt_passage(code, res)
+        if res_passage:
+            return res_passage
+
         # Elements UE
         res_ue = self.search_elt_ue(code, res)
-        if res_ue != {}:
+        if res_ue:
             return res_ue
 
         # Elements Modules
@@ -403,6 +409,25 @@ class ApoEtud(dict):
                     return VOID_APO_RES
         return {}  # no UE result found for this code
 
+    def search_elt_passage(self, code: str, res: NotesTableCompat) -> dict:
+        """Cherche un résultat de type "passage" pour ce code Apogée.
+        dict vide si pas de résultat trouvé pour ce code.
+        L'élement est rempli si:
+        - code est dans les codes passage du formsemestre (sem)
+        - autorisation d'inscription enregistre de sem vers sem d'indice suivant
+        """
+        if res.formsemestre.semestre_id < 1:
+            return {}
+        next_semestre_id = res.formsemestre.semestre_id + 1
+        if code in res.formsemestre.get_codes_apogee(category="passage"):
+            if next_semestre_id in res.get_autorisations_inscription().get(
+                self.etud.id, set()
+            ):
+                return dict(
+                    N="", B=20, J="", R=ScoDocSiteConfig.get_code_apo("ADM"), M=""
+                )
+        return {}
+
     def comp_elt_semestre(self, nt: NotesTableCompat, decision: dict, etudid: int):
         """Calcul résultat apo semestre.
         Toujours vide pour en BUT/APC.
@@ -703,7 +728,8 @@ class ApoData:
             filename = self.orig_filename or e.filename
             raise ScoFormatError(
                 f"""<h3>Erreur lecture du fichier Apogée <tt>{filename}</tt></h3>
-                <p>{e.args[0]}</p>"""
+                <p>{e.args[0]}</p>""",
+                safe=True,
             ) from e
         self.etape_apogee = self.get_etape_apogee()  #  'V1RT'
         self.vdi_apogee = self.get_vdi_apogee()  # '111'
@@ -795,7 +821,9 @@ class ApoData:
             self.sems_periode = None
 
     def get_etape_apogee(self) -> str:
-        """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.
+        Le VET doit être parmi les colonnes de la section XX-APO_COLONNES-XX
+        """
         for elt in self.apo_csv.apo_elts.values():
             if elt.type_objet == "VET":
                 return elt.code
@@ -860,7 +888,8 @@ class ApoData:
             log(f"Colonnes presentes: {present}")
             raise ScoFormatError(
                 f"""Fichier Apogee invalide<br>Colonnes declarees: <tt>{declared}</tt>
-            <br>Colonnes presentes: <tt>{present}</tt>"""
+            <br>Colonnes presentes: <tt>{present}</tt>""",
+                safe=True,
             ) from exc
         # l'ensemble de tous les codes des elements apo des semestres:
         sem_elems = reduce(set.union, list(self.get_codes_by_sem().values()), set())
diff --git a/app/scodoc/sco_apogee_reader.py b/app/scodoc/sco_apogee_reader.py
index 44ab7fa65..32c7f8728 100644
--- a/app/scodoc/sco_apogee_reader.py
+++ b/app/scodoc/sco_apogee_reader.py
@@ -299,11 +299,14 @@ class ApoCSVReadWrite:
                 for i, field in enumerate(fields):
                     cols[self.col_ids[i]] = field
             except IndexError as exc:
-                raise
                 raise ScoFormatError(
                     f"Fichier Apogee incorrect (colonnes excédentaires ? (<tt>{i}/{field}</tt>))",
                     filename=self.get_filename(),
+                    safe=True,
                 ) from exc
+            # Ajoute colonnes vides manquantes, pratique si on a édité le fichier Apo à la main...
+            for i in range(len(fields), len(self.col_ids)):
+                cols[self.col_ids[i]] = ""
             etud_tuples.append(
                 ApoEtudTuple(
                     nip=fields[0],  # id etudiant
@@ -337,6 +340,8 @@ class ApoCSVReadWrite:
             fields = line.split(APO_SEP)
             if len(fields) == 2:
                 k, v = fields
+            elif len(fields) == 1:
+                k, v = fields[0], ""
             else:
                 log(f"Error read CSV: \nline={line}\nfields={fields}")
                 log(dir(f))
diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py
index 2e61608c3..53b1711c2 100644
--- a/app/scodoc/sco_exceptions.py
+++ b/app/scodoc/sco_exceptions.py
@@ -61,12 +61,12 @@ class ScoValueError(ScoException):
 class ScoPermissionDenied(ScoValueError):
     """Permission non accordée (appli web)"""
 
-    def __init__(self, msg=None, dest_url=None):
+    def __init__(self, msg=None, dest_url=None, safe=False):
         if msg is None:
             msg = f"""Opération non autorisée pour {
                 current_user.get_nomcomplet() if current_user else "?"
             }. Pas la permission, ou objet verrouillé."""
-        super().__init__(msg, dest_url=dest_url)
+        super().__init__(msg, dest_url=dest_url, safe=safe)
 
 
 class ScoBugCatcher(ScoException):
@@ -84,8 +84,8 @@ class InvalidEtudId(NoteProcessError):
 class ScoFormatError(ScoValueError):
     "Erreur lecture d'un fichier fourni par l'utilisateur"
 
-    def __init__(self, msg, filename="", dest_url=None):
-        super().__init__(msg, dest_url=dest_url)
+    def __init__(self, msg, filename="", dest_url=None, safe=False):
+        super().__init__(msg, dest_url=dest_url, safe=safe)
         self.filename = filename
 
 
@@ -95,15 +95,15 @@ class ScoInvalidParamError(ScoValueError):
     (id strings, ...)
     """
 
-    def __init__(self, msg=None, dest_url=None):
+    def __init__(self, msg=None, dest_url=None, safe=False):
         msg = msg or "Adresse invalide. Vérifiez vos signets."
-        super().__init__(msg, dest_url=dest_url)
+        super().__init__(msg, dest_url=dest_url, safe=safe)
 
 
 class ScoPDFFormatError(ScoValueError):
     "erreur génération PDF (templates platypus, ...)"
 
-    def __init__(self, msg, dest_url=None):
+    def __init__(self, msg, dest_url=None, safe=False):
         super().__init__(
             f"""Erreur dans un format pdf:
             <p>{msg}</p>
@@ -112,6 +112,7 @@ class ScoPDFFormatError(ScoValueError):
             </p>
             """,
             dest_url=dest_url,
+            safe=safe,
         )
 
 
@@ -130,33 +131,33 @@ class ScoConfigurationError(ScoValueError):
 class ScoLockedFormError(ScoValueError):
     "Modification d'une formation verrouillée"
 
-    def __init__(self, msg="", dest_url=None):
+    def __init__(self, msg="", dest_url=None, safe=False):
         msg = (
             "Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). "
             + str(msg)
         )
-        super().__init__(msg=msg, dest_url=dest_url)
+        super().__init__(msg=msg, dest_url=dest_url, safe=safe)
 
 
 class ScoLockedSemError(ScoValueError):
     "Modification d'un formsemestre verrouillé"
 
-    def __init__(self, msg="", dest_url=None):
+    def __init__(self, msg="", dest_url=None, safe=False):
         msg = "Ce semestre est verrouillé ! " + str(msg)
-        super().__init__(msg=msg, dest_url=dest_url)
+        super().__init__(msg=msg, dest_url=dest_url, safe=safe)
 
 
 class ScoNonEmptyFormationObject(ScoValueError):
     """On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent"""
 
-    def __init__(self, type_objet="objet'", msg="", dest_url=None):
+    def __init__(self, type_objet="objet'", msg="", dest_url=None, safe=False):
         msg = f"""<h3>{type_objet} "{msg}" utilisé(e) dans des semestres: suppression impossible.</h3>
             <p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce {type_objet}).
             Mais il est peut-être préférable de laisser ce programme intact et d'en créer une
             nouvelle version pour la modifier sans affecter les semestres déjà en place.
             </p>
             """
-        super().__init__(msg=msg, dest_url=dest_url)
+        super().__init__(msg=msg, dest_url=dest_url, safe=safe)
 
 
 class ScoInvalidIdType(ScoValueError):
-- 
GitLab