[Scodoc-devel] [SVN] Scolar : [1510] dev en cours sur exports Apo

eviennet at lipn.univ-paris13.fr eviennet at lipn.univ-paris13.fr
Dim 3 Juil 23:13:07 CEST 2016


Une pièce jointe HTML a été nettoyée...
URL: <https://www-rt.iutv.univ-paris13.fr/pipermail/scodoc-devel/attachments/20160703/8f66312f/attachment-0001.html>
-------------- section suivante --------------
Modified: branches/ScoDoc7/ZNotes.py
===================================================================
--- branches/ScoDoc7/ZNotes.py	2016-06-26 21:48:03 UTC (rev 1509)
+++ branches/ScoDoc7/ZNotes.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -85,6 +85,7 @@
 import sco_archives
 import sco_apogee_csv
 import sco_sem_apogee_view
+import sco_semset
 
 from sco_pdf import PDFLOCK
 from notes_table import *
@@ -2697,7 +2698,22 @@
     security.declareProtected(ScoView, 'apo_csv_export_results')
     apo_csv_export_results = sco_sem_apogee_view.apo_csv_export_results
 
+    # sco_semset
+    security.declareProtected(ScoEditApo, 'semset_status')
+    semset_status = sco_semset.semset_status
     
+    security.declareProtected(ScoEditApo, 'semset_page')
+    semset_page = sco_semset.semset_page
+    
+    security.declareProtected(ScoEditApo, 'do_semset_create')
+    do_semset_create = sco_semset.do_semset_create
+
+    security.declareProtected(ScoEditApo, 'do_semset_add_sem')
+    do_semset_add_sem = sco_semset.do_semset_add_sem
+
+    security.declareProtected(ScoEditApo, 'do_semset_remove_sem')
+    do_semset_remove_sem = sco_semset.do_semset_remove_sem
+    
     # ------------- INSCRIPTIONS: PASSAGE D'UN SEMESTRE A UN AUTRE
     security.declareProtected(ScoEtudInscrit,'formsemestre_inscr_passage')
     formsemestre_inscr_passage = sco_inscr_passage.formsemestre_inscr_passage

Modified: branches/ScoDoc7/ZScolar.py
===================================================================
--- branches/ScoDoc7/ZScolar.py	2016-06-26 21:48:03 UTC (rev 1509)
+++ branches/ScoDoc7/ZScolar.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -588,7 +588,7 @@
             </p>
             """)
         
-        # liste des fomsemestres "courants"
+        # liste des formsemestres "courants"
         if cursems:
             H.append('<h2 class="listesems">Sessions en cours</h2>')
             H.append(self._sem_table(cursems))
@@ -629,6 +629,15 @@
             </ul>
             """)
         #
+        if authuser.has_permission(ScoEditApo,self) and False:  # disabled during devel
+            H.append("""<hr>
+            <h3>Exports Apogée</h3>
+            <ul>
+            <li><a class="stdlink" href="Notes/semset_page">Années scolaires / ensembles de semestres</a></li>
+            <li><a  class="stdlink" href="XXX">Charger maquettes Apogée</a></li>
+            </ul>
+            """)
+        #
         return self.sco_header(REQUEST)+'\n'.join(H)+self.sco_footer(REQUEST)
 
     def _sem_table(self, sems):

Modified: branches/ScoDoc7/config/postupgrade-db.py
===================================================================
--- branches/ScoDoc7/config/postupgrade-db.py	2016-06-26 21:48:03 UTC (rev 1509)
+++ branches/ScoDoc7/config/postupgrade-db.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -430,15 +430,17 @@
     # SemSet:
     check_table( cnx, 'notes_semset', [
         """CREATE TABLE notes_semset  (
-    semset_id text default notes_newid('Md') PRIMARY KEY,
-    title text                   
-    );""", ] )
+    semset_id text default notes_newid('NSS') PRIMARY KEY,
+    title text,
+    int annee_scolaire default NULL, -- 2016
+    int sem_id default NULL -- 0, 1, 2
+    ) WITH OIDS;""", ] )
     check_table( cnx, 'notes_semset_formsemestre', [
         """CREATE TABLE notes_semset_formsemestre (
     formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id) ON DELETE CASCADE,
     semset_id text REFERENCES notes_semset (semset_id) ON DELETE CASCADE,
     PRIMARY KEY (formsemestre_id, semset_id)
-    );""", ] )
+    ) WITH OIDS;""", ] )
     # Add here actions to performs after upgrades:
     
     cnx.commit()

Modified: branches/ScoDoc7/misc/createtables.sql
===================================================================
--- branches/ScoDoc7/misc/createtables.sql	2016-06-26 21:48:03 UTC (rev 1509)
+++ branches/ScoDoc7/misc/createtables.sql	2016-07-03 21:13:07 UTC (rev 1510)
@@ -374,15 +374,17 @@
 
 -- semsets
 CREATE TABLE notes_semset (
-   semset_id text default notes_newid('Md') PRIMARY KEY,
-   title text                   
-);
+   semset_id text default notes_newid('NSS') PRIMARY KEY,
+   title text,
+   int annee_scolaire default NULL, -- 2016
+   int sem_id default NULL -- 0, 1, 2
+) WITH OIDS;
 
 CREATE TABLE notes_semset_formsemestre (
    formsemestre_id text REFERENCES notes_formsemestre(formsemestre_id) ON DELETE CASCADE,
    semset_id text REFERENCES notes_semset (semset_id) ON DELETE CASCADE,
    PRIMARY KEY (formsemestre_id, semset_id)
-);
+) WITH OIDS;
 
 -- Coef des UE capitalisees arrivant dans ce semestre:
 CREATE TABLE notes_formsemestre_uecoef (

Modified: branches/ScoDoc7/sco_apogee_csv.py
===================================================================
--- branches/ScoDoc7/sco_apogee_csv.py	2016-06-26 21:48:03 UTC (rev 1509)
+++ branches/ScoDoc7/sco_apogee_csv.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -83,6 +83,8 @@
 """
 from cStringIO import StringIO
 from zipfile import ZipFile
+import collections
+import pprint
 
 import sco_formsemestre
 import sco_formsemestre_status
@@ -125,6 +127,8 @@
         self.code = cols[0]['Code']
     def append(self, col):
         self.cols.append(col)
+    def __str__(self):
+        return "ApoElt(code='%s', cols=%s)" % (self.code, pprint.pformat(self.cols))
 
 class EtuCol:
     """Valeurs colonnes d'un element pour un etudiant"""
@@ -349,11 +353,12 @@
         return L
     
     def get_etape_apogee(self):
-        for elt in self.apo_elts:
-            if elt['Type Objet'] == 'VET':
-                return elt.code
-        raise ScoValueError('Pas de code etape Apogee (manque élément VET)')
-
+        return self.apo_elts.keys()[0]
+        # for elt in self.apo_elts:
+        #     if elt['Type Objet'] == 'VET':
+        #         return elt.code
+        # raise ScoValueError('Pas de code etape Apogee (manque élément VET)')
+    
     def get_annee_scolaire(self):
         """Annee scolaire du fichier Apogee: un integer
         = annee du mois de septembre de début

Modified: branches/ScoDoc7/sco_archives.py
===================================================================
--- branches/ScoDoc7/sco_archives.py	2016-06-26 21:48:03 UTC (rev 1509)
+++ branches/ScoDoc7/sco_archives.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -30,7 +30,7 @@
 
  Archives are plain files, stored in 
     <INSTANCE_HOME>/var/scodoc/archives/<deptid>
- (where <INSTANCE_HOME> is usually /opt/scodoc/instance, and <depid> a departement id)
+ (where <INSTANCE_HOME> is usually /opt/scodoc/instance, and <deptid> a departement id)
 
  Les PV de jurys et documents associés sont stockées dans un sous-repertoire de la forme
     <archivedir>/<dept>/<formsemestre_id>/<YYYY-MM-DD-HH-MM-SS>
@@ -39,7 +39,7 @@
     <archivedir>/docetuds/<dept>/<etudid>/<YYYY-MM-DD-HH-MM-SS>
 
  Les maquettes Apogée pour l'export des notes sont dans
-    <archivedir>/apo_csv/<dept>/<annee_scolaire>:<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
+    <archivedir>/apo_csv/<dept>/<annee_scolaire>-<sem_id>/<YYYY-MM-DD-HH-MM-SS>/<code_etape>.csv
     
  Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt
  qui est une description (humaine, format libre) de l'archive.
@@ -104,7 +104,8 @@
         :return: list of archive oids
         """
         base = os.path.join(self.root, context.DeptId()) + os.path.sep
-        dirs = glob.glob( baseXXXXXX ) # <<<<<< TODO XXX
+        dirs = glob.glob(base+'*')
+        return [ os.path.split(x)[1] for x in dirs ]
     
     def list_obj_archives(self, context, oid):
         """Returns

Copied: branches/ScoDoc7/sco_etape_apogee.py (from rev 1508, branches/ScoDoc7/sco_sem_apogee.py)
===================================================================
--- branches/ScoDoc7/sco_etape_apogee.py	                        (rev 0)
+++ branches/ScoDoc7/sco_etape_apogee.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -0,0 +1,305 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 2001 - 2016 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 at 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
+   => /opt/scodoc/var/scodoc/archives/apo_csv/RT/2016-1/2016-07-03-16-12-19/V3ASR.csv 
+   pour une maquette de l'année scolaire 2016, semestre 1, etape V3ASR
+   
+   API:
+   apo_csv_store(context, csv, annee_scolaire, sem_id)        
+      store maq file (archive)
+      
+   apo_csv_get(context, etape_apo, annee_scolaire, sem_id)
+      get maq data (read stored file and returns string)
+
+   apo_csv_delete(context, etape_apo, annee_scolaire, sem_id)
+
+   apo_csv_list_stored_etapes(context, annee_scolaire=None, sem_id=None) 
+       returns: liste des codes etapes stockés (et annee_scolaire, sem_id)
+
+x   apo_csv_check(context, sem)
+x      check students in stored maqs vs students in sem
+x      Cas à détecter:
+x      - etudiants ScoDoc sans code NIP
+x      - etudiants dans sem (ScoDoc) mais dans aucun CSV
+x      - etudiants dans un CSV mais pas dans sem ScoDoc
+x      - etudiants dans plusieurs CSV (argh!)
+x      
+x      returns: etuds_ok (in ScoDoc and CSVs)
+x               etuds_no_apo
+x               unknown_apo : liste de { 'NIP', 'nom', 'prenom' }
+x               dups_apo : liste de { 'NIP', 'nom', 'prenom', 'etapes_apo' }
+x               etapes_missing_csv : liste des étapes du semestre sans maquette CSV
+x
+x   apo_csv_check_etape(context, sem, etape_apo)
+x      check une etape
+      
+"""
+
+from sco_utils import *
+from notes_log import log
+import sco_formsemestre
+import notes_table
+import sco_groups
+import sco_groups_view
+import sco_archives
+import sco_apogee_csv
+
+class ApoCSVArchiver(sco_archives.BaseArchiver):
+    def __init__(self):
+        sco_archives.BaseArchiver.__init__(self, archive_type='apo_csv')
+
+ApoCSVArchive = ApoCSVArchiver()
+
+
+# def get_sem_apo_archive(context, formsemestre_id):
+#     """Get, or create if necessary, the archive for apo CSV files"""
+    
+#     archive_id 
+    
+#     return archive_id
+
+def apo_csv_store(context, REQUEST, csv_data, annee_scolaire, sem_id):
+    """
+    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é
+    """
+    # sanity check
+    filesize = len(csv_data)
+    if filesize < 10 or filesize > CONFIG.ETUD_MAX_FILE_SIZE:
+        raise ScoValueError('Fichier csv de taille invalide ! (%d)' % filesize)
+    
+    apo_data = sco_apogee_csv.ApoData(csv_data) # parse le fichier -> exceptions
+    etape_apogee = apo_data.etape_apogee
+    filename = etape_apogee + '.csv'
+    
+    if etape_apogee in apo_csv_list_stored_etapes(context, annee_scolaire, sem_id=sem_id):
+        raise ScoValueError('Etape %s déjà stockée !' % etape_apogee)
+    
+    oid = '%d-%d' % (annee_scolaire, sem_id)
+    description = '%s;%s;%s' % (etape_apogee,annee_scolaire,sem_id)
+    archive_id = ApoCSVArchive.create_obj_archive(context, oid, description )
+    ApoCSVArchive.store(archive_id, filename, csv_data )
+    
+    return etape_apogee
+
+
+def apo_csv_list_stored_archives(context, annee_scolaire=None, sem_id=None):
+    """
+    :return: list of informations about stored CSV
+    """
+    oids = ApoCSVArchive.list_oids(context) # [ '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]*-' + str(annee_scolaire) )
+        oids = [ x for x in oids if e.match(x) ]
+    
+    infos = [] # liste d'infos
+    for oid in oids:
+        archive_ids = ApoCSVArchive.list_obj_archives(context, oid)
+        for archive_id in archive_ids:
+            description = ApoCSVArchive.get_archive_description(archive_id)
+            etape_apo, annee_scolaire, sem_id = description.split(';')
+            infos.append( {
+                'archive_id' : archive_id,
+                'annee_scolaire' : int(annee_scolaire),
+                'sem_id' : int(sem_id),
+                'etape_apo' : etape_apo,
+                'date' : ApoCSVArchive.get_archive_date(archive_id)
+                } ) 
+    
+    return infos
+    
+def apo_csv_list_stored_etapes(context, annee_scolaire, sem_id=None):
+    """
+    :return: list of stored etapes [{}]
+    """
+    return apo_csv_list_stored_archives(context, annee_scolaire=annee_scolaire, sem_id=sem_id)
+
+def apo_csv_delete(context, archive_id):
+    """Delete archived CSV
+    """    
+    ApoCSVArchive.delete_archive(archive_id)
+
+def apo_csv_get(context, etape_apo, annee_scolaire='', sem_id=''):
+    """Get CSV data for given etape_apo
+    :return: CSV, as a data string
+    """
+    stored_archives = apo_csv_list_stored_etapes(context, annee_scolaire=annee_scolaire, sem_id=sem_id)
+    for info in stored_archives:
+        if info['etape_apo'] == etape_apo:
+            archive_id = info['archive_id']
+            data = ApoCSVArchive.get(archive_id, etape_apo + '.csv')
+            return data
+    
+    raise ScoValueError('Etape %s non enregistree (%s, %s)' % (etape_apo, annee_scolaire, sem_id))
+    
+# ------------------------------------------------------------------------
+
+def apo_get_sem_etapes(context, 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: set of etape_apo.
+    """
+    etapes = set()
+    for key in ('etape_apo', 'etape_apo2', 'etape_apo3', 'etape_apo4'):
+        if sem[key]:
+            etapes.add(sem[key])
+    return etapes
+
+def apo_csv_check_etape(context, sem, sem_nips, etape_apo):
+    """Check etape
+    """
+    # Etudiants dans la maquette CSV:
+    csv_data = apo_csv_get(context, sem, etape_apo)
+    ApoData = sco_apogee_csv.apo_read_csv(csv_data)
+    apo_nips = set([ e['nip'] for e in ApoData['etuds'] ])
+    #
+    nips_ok = sem_nips.intersection(apo_nips)
+    nips_no_apo = sem_nips - apo_nips # dans ScoDoc mais pas dans Apogée
+    nips_no_sco = apo_nips - sem_nips # dans Apogée mais pas dans ScoDoc
+    
+    return nips_ok, apo_nips, nips_no_apo, nips_no_sco # ok si nips_no_apo et nips_no_sco sont vides
+
+def apo_csv_check(context, sem):
+    """
+    check students in stored maqs vs students in sem
+      Cas à détecter:
+      - étapes du semestre sans maquette CSV (etapes_missing_csv)
+      - etudiants ScoDoc sans code NIP (etuds_without_nip)
+      - etudiants dans sem (ScoDoc) mais dans aucun CSV (nips_no_apo)
+      - etudiants dans un CSV mais pas dans sem ScoDoc (nips_no_sco)
+      - etudiants dans plusieurs CSV (argh!)
+    """
+    formsemestre_id = sem['formsemestre_id']
+
+    # Etapes du semestre sans maquette CSV:
+    etapes_sem = apo_get_sem_etapes(context, sem)
+    etapes_apo = set(apo_csv_list_stored_etapes(context, sem))
+    etapes_missing_csv = etapes_sem - etapes_apo
+
+    # Etudiants inscrits dans ce semestre:
+    nt = context._getNotesCache().get_NotesTable(context, formsemestre_id)   
+    sem_etuds = nt.identdict.values()
+    sem_nips = set([ e['code_nip'] for e in sem_etuds if e['code_nip'] ])
+    
+    etuds_without_nip = [ e for e in sem_etuds if not e['code_nip'] ]
+        
+    # 
+    nips_ok = set()     # codes nip des etudiants dans ScoDoc et Apogée
+    nips_no_apo = sem_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
+    for etape_apo in etapes_apo:
+        et_nips_ok, et_apo_nips, et_nips_no_apo, et_nips_no_sco = apo_csv_check_etape(context, sem, sem_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)
+
+    # 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) 
+                      and (not etuds_without_nip) 
+                      and (not nips_no_apo)
+                      and (not apo_dups) )
+    
+    return ok_for_export, etapes_missing_csv, etuds_without_nip, nips_ok, nips_no_apo, nips_no_sco, apo_dups
+
+
+def apo_csv_retreive_etuds_by_nip(context, sem, 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(context, sem)
+    for etape_apo in etapes_apo:
+        csv_data = apo_csv_get(context, sem, etape_apo)
+        ApoData = sco_apogee_csv.apo_read_csv(csv_data)
+        etape_apo = ApoData['etape_apogee']
+        for e in ApoData['etuds']:
+            e['etape_apo'] = etape_apo
+        apo_etuds_by_nips.update( dict( [ (e['nip'], e) for e in ApoData['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 sco_sem_apogee import *
+import sco_groups
+import sco_groups_view
+import sco_formsemestre
+from sco_etape_apogee import *
+
+context = go_dept(app, 'RT')
+#sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+
+csv_data = open('/opt/misc/VL4RT_V3ASR.TXT').read()
+annee_scolaire=2016
+sem_id=1
+
+print apo_csv_list_stored_archives(context)
+
+
+apo_csv_store(context, REQUEST, csv_data, annee_scolaire, sem_id)
+
+
+
+groups_infos = sco_groups_view.DisplayedGroupsInfos(context, [sco_groups.get_default_group(context, formsemestre_id)], formsemestre_id=formsemestre_id, REQUEST=REQUEST)
+
+nt = context.Notes._getNotesCache().get_NotesTable(context.Notes, formsemestre_id)
+
+
+"""

Copied: branches/ScoDoc7/sco_etape_apogee_view.py (from rev 1508, branches/ScoDoc7/sco_sem_apogee_view.py)
===================================================================
--- branches/ScoDoc7/sco_etape_apogee_view.py	                        (rev 0)
+++ branches/ScoDoc7/sco_etape_apogee_view.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -0,0 +1,450 @@
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 2001 - 2016 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 at viennet.net
+#
+##############################################################################
+
+"""ScoDoc : formulaires gestion maquettes Apogee / export resultats
+"""
+
+from cStringIO import StringIO
+from zipfile import ZipFile
+
+from sco_utils import *
+from notes_log import log
+import sco_formsemestre
+import sco_formsemestre_status
+import notes_table
+from gen_tables import GenTable
+from sco_formsemestre_edit import can_edit_sem
+import sco_sem_apogee
+import sco_apogee_csv
+
+
+
+def apo_maq_status(context, formsemestre_id, REQUEST=None):
+    """Page statut / tableau de bord
+    
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    can_edit = can_edit_sem(context, REQUEST, formsemestre_id, sem=sem)
+    
+    tab_archives = table_apo_csv_list(context, formsemestre_id, REQUEST=REQUEST)
+
+    ok_for_export, etapes_missing_csv, etuds_without_nip, nips_ok, nips_no_apo, nips_no_sco, apo_dups = sco_sem_apogee.apo_csv_check(context, sem)
+    
+    
+    H = [ context.sco_header(REQUEST, page_title='Export Apogée' ),
+          '''<h2>Préparation export Apogée</h2>'''
+          '''<div class="apo_csv_1"><span>Etapes du semestre:</span> <b><tt>%s</tt></b>''' % sco_formsemestre_status.formsemestre_etape_apo_str(sem)
+    ]
+    if can_edit:
+        H.append( ''' (<a href="formsemestre_editwithmodules?formation_id=%(formation_id)s&formsemestre_id=%(formsemestre_id)s">modifier</a>)''' % sem )
+    H.append('</div>')
+    
+    # Maquettes enregistrées
+    H.append('<div class="apo_csv_list">')
+    if not tab_archives.is_empty():
+        H.append( tab_archives.html() )
+    else:
+        H.append( '''<p><em>Aucune maquette chargée</em></p>''' )
+    if can_edit:
+        H.append('''<form id="apo_csv_add" action="view_apo_csv_store" method="post" enctype="multipart/form-data">
+        <input type="file" size="30" name="csvfile"/>
+        <input type="hidden" name="formsemestre_id" value="%s"/>
+        <input type="submit" value="Ajouter ce fichier"/>
+        </form>''' % (formsemestre_id,)
+        )
+    H.append('</div>')    
+    
+    # Tableau de bord
+    if ok_for_export:
+        class_ok = 'apo_csv_status_ok'
+    else:
+        class_ok = 'apo_csv_status_nok'
+    
+    H.append('<div class="apo_csv_status %s">' % class_ok)
+    if ok_for_export:
+        H.append('<ul>')
+        if nips_no_sco: # seulement un warning
+            url_list = 'view_apo_etuds?formsemestre_id=%s&title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20ScoDoc:&nips=%s' % (formsemestre_id, '&nips='.join(nips_no_sco) )
+            H.append('<li class="apo_csv_warning">Attention: il y a <a href="%s">%d étudiant(s)</a> dans les maquettes Apogée chargées non inscrit(s) dans ce semestre ScoDoc;</li>' % (url_list, len(nips_no_sco)) )
+        
+        H.append('''<li>%d étudiants, prêt pour l'export.</li>'''%len(nips_ok))
+        H.append('<ul>')
+    else:
+        H.append('''<span>Problèmes à résoudre avant export des résultats:</span>''')
+        H.append('<div class="apo_csv_problems"><ul>')
+        
+        if etapes_missing_csv:
+            H.append('<li>Etapes sans maquette: <tt>%s</tt></li>' 
+                     % ', '.join(sorted(etapes_missing_csv)) )
+        if etuds_without_nip:
+            H.append('<li>%d étudiants ScoDoc sans code NIP</li>' % len(etuds_without_nip))
+        
+        if nips_no_apo:
+            url_list = 'view_scodoc_etuds?formsemestre_id=%s&title=Etudiants%%20ScoDoc%%20non%%20listés%%20dans%%20les%%20maquettes%%20Apogée%%20chargées&nips=%s' % (formsemestre_id, '&nips='.join(nips_no_apo) )
+            H.append('<li><a href="%s">%d étudiants</a> dans ce semestre non présents dans les maquettes Apogée chargées</li>' % (url_list, len(nips_no_apo)) )
+        
+        if nips_no_sco: # seulement un warning
+            url_list = 'view_apo_etuds?formsemestre_id=%s&title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20ScoDoc:&nips=%s' % (formsemestre_id, '&nips='.join(nips_no_sco) )
+            H.append('<li class="apo_csv_warning">Attention: il reste <a href="%s">%d étudiants</a> dans les maquettes Apogée chargées mais pas inscrits dans ce semestre ScoDoc</li>' % (url_list, len(nips_no_sco)) )
+
+        if apo_dups:
+            url_list = 'view_apo_etuds?formsemestre_id=%s&title=Doublons%%20Apogee&nips=%s' % (formsemestre_id, '&nips='.join(apo_dups) )
+            H.append('<li><a href="%s">%d étudiants</a> présents dans les <em>plusieurs</em> maquettes Apogée chargées</li>' % (url_list, len(apo_dups)) )
+        
+        H.append('</ul></div>')
+    H.append('</div>')
+
+    # Decisions de jury
+    nt = context._getNotesCache().get_NotesTable(context, formsemestre_id) 
+    jury_ok = nt.all_etuds_have_sem_decisions()
+
+    if jury_ok:
+        class_ok = 'apo_csv_jury_ok'
+    else:
+        class_ok = 'apo_csv_jury_nok'
+    H.append('<div class="apo_csv_jury %s">' % class_ok)
+    if jury_ok:
+         H.append('''<ul><li>Décisions de jury saisies</li></ul>''')
+         if ok_for_export:
+             H.append('''<form action="apo_csv_export_results" method="get">
+             <input type="submit" value="Export vers Apogée">
+             <input type="hidden" name="formsemestre_id" value="%s"/>
+             </form>''' % (formsemestre_id,))
+    else:
+        H.append('''<ul><li>Il manque des décisions de jury !</li></ul>''')
+    H.append('<div>')
+
+    # Aide:
+    H.append('''<div class="pas_help">
+    <h3>Explications</h3>
+    <p>Cette page permet de stocker les fichiers Apogée nécessaires pour 
+    l'export des résultats après les jurys, puis de remplir et exporter ces fichiers.
+    </p>
+    <p>
+    Les fichiers ("maquettes") Apogée sont de type CSV, du texte codé en %s.
+    </p>
+    <p>On a un fichier par étape Apogée. Pour les obtenir, se débrouiller avec 
+    Apogée. Leur contenu ressemble à cela:</p>
+    <pre class="small_pre_acc">
+ 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 (...)
+ 11681234	DUPONT	TOTO	 23/09/1986	18	20	ADM	18	20	ADM	(...)
+    </pre>
+    <p>Après avoir obtenu les fichier, stockez les dans ScoDoc 
+    (bouton "Ajouter fichier" en haut de cette page. Après vérification, il va 
+    apparaitre dans une table. Vous pouvez supprimer ce fichier, ou en ajouter 
+    d'autres si votre semestre correspond à plusieurs étapes Apogée.
+    </p>
+    <p>ScoDoc vérifie que tous les étudiants du semestre sont mentionnés dans 
+    un fichier Apogée et que les étapes correspondent.</p>
+    <p>Lorsque c'est le cas, et que les décisions de jury sont saisies, 
+    un bouton "Export vers Apogée" apparait et vous pouvez exporter les résultats.
+    <p>
+    <p>Vous obtiendrez alors un fichier ZIP comprenant tous les fichiers nécessaires.
+    Certains de ces fichiers devront être importés dans Apogée.
+    </p>
+    </div>
+    ''' % (sco_apogee_csv.APO_ENCODING,) )
+    H.append( context.sco_footer(REQUEST) )
+    return '\n'.join(H)
+
+
+def table_apo_csv_list(context, formsemestre_id, REQUEST=None):
+    """Table des archives (triée par date d'archivage)
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    can_edit = can_edit_sem(context, REQUEST, formsemestre_id, sem=sem)
+    T = sco_sem_apogee.apo_csv_list_stored_archives(context, sem).values()
+    T.sort(key=lambda x: x['date'])
+    for t in T:
+        # Ajoute qq infos pour affichage:
+        csv_data = sco_sem_apogee.apo_csv_get(context, sem, t['etape_apo'])
+        ApoData = sco_apogee_csv.apo_read_csv(csv_data)
+        t['filename'] = ApoData['titres']['apoC_Fichier_Exp']
+        t['nb_etuds'] = len(ApoData['etuds'])    
+        t['date_str'] = t['date'].strftime('%d/%m/%Y à %H:%M')
+        view_link = 'view_apo_csv?formsemestre_id=%s&etape_apo=%s' % (formsemestre_id, t['etape_apo'])
+        t['_filename_target'] = view_link
+        t['_etape_apo_target'] = view_link
+        t['suppress'] = icontag('delete_small_img', border='0', alt='supprimer', title='Supprimer')
+        t['_suppress_target'] = 'view_apo_csv_delete?formsemestre_id=%s&etape_apo=%s' % (formsemestre_id, t['etape_apo'])
+        
+    columns_ids=['filename', 'etape_apo', 'date_str', 'nb_etuds']
+    if can_edit:
+        columns_ids = ['suppress'] + columns_ids
+    
+    tab = GenTable( 
+        titles={ 
+            'archive_id' : '',
+            'filename' : 'Fichier',
+            'etape_apo' : 'Etape',
+            'nb_etuds' : 'Nb étudiants',
+            'date_str' : 'Enregistré le',            
+        },
+        columns_ids=columns_ids,
+        rows = T,
+        html_class='gt_table table_leftalign apo_maq_list',
+        html_sortable=True,
+        #base_url = '%s?formsemestre_id=%s' % (REQUEST.URL0, formsemestre_id),
+        #caption='Maquettes enregistrées',
+        preferences=context.get_preferences(formsemestre_id))
+    
+    return tab
+
+def view_apo_etuds(context, formsemestre_id, title='', nips=[], 
+                   format='html', REQUEST=None):
+    """Table des étudiants Apogée par nips
+    """
+    if nips and type(nips) != type([]):
+        nips = [nips]
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    etuds = sco_sem_apogee.apo_csv_retreive_etuds_by_nip(context, sem, nips)
+    
+    return _view_etuds_page(context, formsemestre_id, title=title, etuds=etuds.values(), 
+                            keys = ('nip', 'etape_apo', 'nom', 'prenom'),
+                            format=format, REQUEST=REQUEST)
+
+def view_scodoc_etuds(context, formsemestre_id, title='', etudids=None, nips=None,
+                      format='html', REQUEST=None):
+    """Table des étudiants ScoDoc par nips ou etudids
+    """
+    if etudids is not None:
+        if type(etudids) != type([]):
+            etudids = [etudids]
+        etuds = [ context.getEtudInfo(etudid=etudid, filled=True)[0] for etudid in etudids ]
+    elif nips is not None:
+        if type(nips) != type([]):
+            nips = [nips]
+        etuds = [ context.getEtudInfo(code_nip=nip, filled=True)[0] for nip in nips ]
+    else:
+        raise ValueError('etudid or NIP must be specified')
+
+    for e in etuds:
+        tgt = 'ficheEtud?etudid=' + e['etudid']
+        e['_nom_target'] = tgt
+        e['_prenom_target'] = tgt
+        e['_nom_td_attrs'] = 'id="%s" class="etudinfo"' % (e['etudid'],)
+        e['_prenom_td_attrs'] = 'id="pre-%s" class="etudinfo"' % (e['etudid'],)
+    
+    return _view_etuds_page(context, formsemestre_id, title=title, etuds=etuds, 
+                            keys=('code_nip', 'nom', 'prenom'),
+                            format=format, REQUEST=REQUEST)
+
+
+def _view_etuds_page(context, formsemestre_id, title='', etuds=[], 
+                     keys=(),
+                     format='html', REQUEST=None):
+    # Tri les étudiants par nom:
+    if etuds:
+        etuds.sort( key=lambda x: (x['nom'], x['prenom']) )
+    
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    H = [ context.sco_header(REQUEST, page_title=title,
+                             init_qtip = True,
+                             javascripts=['js/etud_info.js'] ),
+          '<h2>%s</h2>' % title
+          ]
+    
+    tab = GenTable( 
+        titles={ 
+            'nip' : 'Code NIP',
+            'code_nip' : 'Code NIP',
+            'etape_apo' : 'Etape',
+            'nom' : 'Nom',
+            'prenom' : 'Prénom',
+        },
+        columns_ids=keys, 
+        rows = etuds,
+        html_sortable=True,
+        html_class='gt_table table_leftalign',
+        filename='students_apo',
+        preferences=context.get_preferences(formsemestre_id)
+    )
+    if format != 'html':
+        return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+    H.append( tab.html() )
+    
+    return '\n'.join(H) + context.sco_footer(REQUEST) 
+
+
+def view_apo_csv_store(context, formsemestre_id, csvfile, REQUEST=None):
+    """Store CSV data
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    data = csvfile.read()
+    filename = csvfile.filename
+    sco_sem_apogee.apo_csv_store(context, REQUEST, sem, data)
+    return REQUEST.RESPONSE.redirect('apo_formsemestre_status?formsemestre_id=' + formsemestre_id)
+
+def view_apo_csv_delete(context, formsemestre_id, etape_apo, dialog_confirmed=False, REQUEST=None):
+    """Delete CSV file
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    # check permission
+    if not can_edit_sem(context, REQUEST, sem=sem):
+        raise AccessDenied('opération non autorisée pour %s' % str(REQUEST.AUTHENTICATED_USER))
+    
+    dest_url = "apo_formsemestre_status?formsemestre_id=" + formsemestre_id
+    if not dialog_confirmed:
+        return context.confirmDialog(
+            """<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
+               <p>La suppression sera définitive.</p>"""
+            % (etape_apo,),
+            dest_url="", REQUEST=REQUEST, cancel_url=dest_url, 
+            parameters={'formsemestre_id' : formsemestre_id, 'etape_apo' : etape_apo })
+    
+    sco_sem_apogee.apo_csv_delete(context, sem, etape_apo, REQUEST=REQUEST)
+    return REQUEST.RESPONSE.redirect(dest_url+'&head_message=Archive%20supprimée')
+
+
+def view_apo_csv(context, formsemestre_id, etape_apo, format='html', REQUEST=None):
+    """Visualise une maquette stockée
+    """
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    csv_data = sco_sem_apogee.apo_csv_get(context, sem, etape_apo)
+    ApoData = sco_apogee_csv.apo_read_csv(csv_data)
+
+    ok_for_export, etapes_missing_csv, etuds_without_nip, nips_ok, nips_no_apo, nips_no_sco, apo_dups = sco_sem_apogee.apo_csv_check(context, sem)
+    
+    H = [ context.sco_header(REQUEST, page_title='Maquette Apogée enregistrée pour %s' % etape_apo,
+                             init_qtip = True,
+                             javascripts=['js/etud_info.js'] ),
+          '<h2>Etudiants dans la maquette Apogée %s</h2>' % etape_apo
+          ]
+    # Infos générales
+    H.append("""
+    <div class="apo_csv_infos">
+    <div class="apo_csv_etape"><span>Code étape:</span><span>%(etape_apogee)s</span></div>
+    <div class="apo_csv_etape"><span>Indices semestres:</span><span>%(semestre_ids)s</span></div>
+    
+    </div>
+    """ % ApoData )
+    
+    # Liste des étudiants (sans les résultats pour le moment): TODO
+    etuds = ApoData['etuds']
+    if not etuds:
+        return '\n'.join(H) + '<p>Aucun étudiant</p>' + context.sco_footer(REQUEST)
+
+    # Ajout infos sur ScoDoc vs Apogee
+    for e in etuds:
+        e['in_scodoc'] = e['nip'] not in nips_no_sco
+        e['in_scodoc_str'] = { True: 'oui', False : 'non' }[e['in_scodoc']]
+        if e['in_scodoc']:
+            e['_in_scodoc_str_target'] = 'ficheEtud?code_nip=' + e['nip']
+            e.update( context.getEtudInfo(code_nip=e['nip'], filled=True)[0] )
+            e['_nom_td_attrs'] = 'id="%s" class="etudinfo"' % (e['etudid'],)
+            e['_prenom_td_attrs'] = 'id="pre-%s" class="etudinfo"' % (e['etudid'],)
+        else:
+            e['_css_row_class'] = 'apo_not_scodoc'
+        
+    # Construit la table:
+    tab = GenTable( 
+        titles={ 
+            'nip' : 'Code NIP',
+            'nom' : 'Nom',
+            'prenom' : 'Prénom',
+            'naissance' : 'Naissance',
+            'in_scodoc_str' : 'Inscrit dans ce semestre ScoDoc',
+        },
+        columns_ids=('nip', 'nom', 'prenom', 'naissance', 'in_scodoc_str'),
+        rows = etuds,
+        html_sortable=True,
+        html_class='gt_table table_leftalign apo_maq_table',
+        base_url = '%s?formsemestre_id=%s&etape_apo=%s' % (REQUEST.URL0, formsemestre_id, etape_apo),
+        filename='students_' + etape_apo,
+        caption='Etudiants Apogée en ' + etape_apo,
+        preferences=context.get_preferences(formsemestre_id)
+    )
+
+    if format != 'html':
+        return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+    H += [
+        tab.html(),
+        '''<div><a href="apo_formsemestre_status?formsemestre_id=%s">Retour</a>    
+        </div>'''
+        % formsemestre_id,
+        context.sco_footer(REQUEST) 
+        ]
+    
+    return '\n'.join(H)
+
+
+def apo_csv_export_results(context, formsemestre_id, REQUEST=None):
+    """Remplit les fichies CSV archivés
+    et donne un ZIP avec tous les résultats.
+    """
+    # nota: on peut éventuellement exporter même si tout n'est pas ok
+    # mais le lien via le tableau de bord n'est pas actif
+    # Les fichiers ne sont pas stockés: pas besoin de permission particulière
+    
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)    
+    data = StringIO()
+    dest_zip = ZipFile( data, 'w' )
+
+    etapes_apo = sco_sem_apogee.apo_csv_list_stored_etapes(context, sem)
+    for etape_apo in etapes_apo:
+        apo_csv = sco_sem_apogee.apo_csv_get(context, sem, etape_apo)
+        sco_apogee_csv.formsemestre_export_to_apogee(
+            context, formsemestre_id, 
+            apo_csv,
+            dest_zip=dest_zip )
+    
+    basename = context.get_preference('DeptName') + sem['anneescolaire'] + sem['titre_num']
+    basename = sco_sem_apogee.ApoCSVArchive.sanitize_filename(unescape_html(basename))
+    
+    dest_zip.close()
+    size = data.tell()
+    content_type = 'application/zip'
+    REQUEST.RESPONSE.setHeader('content-disposition',
+                               'attachement; filename="%s.zip"' % basename  )
+    REQUEST.RESPONSE.setHeader('content-type', content_type)
+    REQUEST.RESPONSE.setHeader('content-length', size)
+    return data.getvalue()

Modified: branches/ScoDoc7/sco_formsemestre.py
===================================================================
--- branches/ScoDoc7/sco_formsemestre.py	2016-06-26 21:48:03 UTC (rev 1509)
+++ branches/ScoDoc7/sco_formsemestre.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -83,7 +83,7 @@
         formsemestre_enrich(context, sem)
     
     # tri par date
-    sems.sort(lambda x,y: cmp(y['dateord'],x['dateord']))
+    sems.sort(lambda x,y: cmp((y['dateord'], y['semestre_id']),(x['dateord'], x['semestre_id'])))
     
     return sems
 

Deleted: branches/ScoDoc7/sco_sem_apogee.py
===================================================================
--- branches/ScoDoc7/sco_sem_apogee.py	2016-06-26 21:48:03 UTC (rev 1509)
+++ branches/ScoDoc7/sco_sem_apogee.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -1,294 +0,0 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 2001 - 2016 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 at viennet.net
-#
-##############################################################################
-
-"""ScoDoc : stockage et vérifications des "maquettes" Apogée 
-   (fichiers CSV pour l'export vers Apogée)
-   associées aux étapes d'un semestre.
-
-   Voir sco_apogee_csv.py pour la structure du fichier Apogée.
-   
-   API:
-   apo_csv_store(context, sem, csv)        
-      store maq file (archive)
-      
-   apo_csv_get(context, sem, etape_apo)
-      get maq data (read stored file and returns string)
-
-   apo_csv_delete(context, sem, etape_apo)
-
-   apo_csv_list_stored_etapes(context, sem) 
-       returns: liste des codes etapes stockés
-
-   apo_csv_check(context, sem)
-      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!)
-      
-      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(context, sem, etape_apo)
-      check une etape
-      
-"""
-
-from sco_utils import *
-from notes_log import log
-import sco_formsemestre
-import notes_table
-from sco_formsemestre_edit import can_edit_sem
-import sco_groups
-import sco_groups_view
-import sco_archives
-import sco_apogee_csv
-
-class ApoCSVArchiver(sco_archives.BaseArchiver):
-    def __init__(self):
-        sco_archives.BaseArchiver.__init__(self, archive_type='apo_csv')
-
-ApoCSVArchive = ApoCSVArchiver()
-
-
-# def get_sem_apo_archive(context, formsemestre_id):
-#     """Get, or create if necessary, the archive for apo CSV files"""
-    
-#     archive_id 
-    
-#     return archive_id
-
-def apo_csv_store(context, REQUEST, sem, csv_data):
-    """
-    
-    :return: etape_apo du fichier CSV stocké (cette étape peut ne pas déjà etre 
-    declarée dans le semestre)
-    """
-    if not can_edit_sem(context, REQUEST, sem=sem):
-        raise AccessDenied('opération non autorisée pour %s' % str(REQUEST.AUTHENTICATED_USER))
-    # sanity check
-    filesize = len(csv_data)
-    if filesize < 10 or filesize > CONFIG.ETUD_MAX_FILE_SIZE:
-        raise ScoValueError('Fichier csv de taille invalide ! (%d)' % filesize)
-    
-    ApoData = sco_apogee_csv.apo_read_csv(csv_data) # parse le fichier -> exceptions
-    etape_apogee = ApoData['etape_apogee']
-    filename = etape_apogee + '.csv'
-
-    if etape_apogee in apo_csv_list_stored_etapes(context, sem):
-        raise ScoValueError('Etape %s déjà stockée !' % etape_apogee)
-    
-    archive_id = ApoCSVArchive.create_obj_archive(context, sem['formsemestre_id'], etape_apogee )
-    ApoCSVArchive.store(archive_id, filename, csv_data )
-    
-    return etape_apogee
-
-
-def apo_csv_list_stored_archives(context, sem):
-    """
-    :return: list of informations about stored CSV
-    """
-    infos = {} # etape_apo : infos
-    for archive_id in ApoCSVArchive.list_obj_archives(context, sem['formsemestre_id']):
-        etape_apo = ApoCSVArchive.get_archive_description(archive_id)
-        infos[etape_apo] = {
-            'archive_id' : archive_id,
-            'etape_apo' : etape_apo,
-            'date' : ApoCSVArchive.get_archive_date(archive_id)
-            } 
-    
-    return infos
-    
-def apo_csv_list_stored_etapes(context, sem):
-    """
-    :return: list of stored etapes
-    """
-    return apo_csv_list_stored_archives(context, sem).keys()
-
-def apo_csv_delete(context, sem, etape_apo='', REQUEST=None):
-    """Delete archived CSV
-    """
-    if not etape_apo:
-        return
-    # check permission
-    if not can_edit_sem(context, REQUEST, sem=sem):
-        raise AccessDenied('opération non autorisée pour %s' % str(REQUEST.AUTHENTICATED_USER))
-
-    for archive_id in ApoCSVArchive.list_obj_archives(context, sem['formsemestre_id']):
-        if etape_apo == ApoCSVArchive.get_archive_description(archive_id):
-            ApoCSVArchive.delete_archive(archive_id)
-
-def apo_csv_get(context, sem, etape_apo):
-    """Get CSV data for given etape_apo
-    :return: CSV, as a data string
-    """
-    stored_archives = apo_csv_list_stored_archives(context, sem)
-    if not etape_apo in stored_archives:
-        raise # XXX
-        raise ScoValueError('Etape %s non enregistree' % etape_apo)
-
-    archive_id = stored_archives[etape_apo]['archive_id']    
-    data = ApoCSVArchive.get(archive_id, etape_apo + '.csv')
-    
-    return data
-
-def apo_get_sem_etapes(context, 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: set of etape_apo.
-    """
-    etapes = set()
-    for key in ('etape_apo', 'etape_apo2', 'etape_apo3', 'etape_apo4'):
-        if sem[key]:
-            etapes.add(sem[key])
-    return etapes
-
-def apo_csv_check_etape(context, sem, sem_nips, etape_apo):
-    """Check etape
-    """
-    # Etudiants dans la maquette CSV:
-    csv_data = apo_csv_get(context, sem, etape_apo)
-    ApoData = sco_apogee_csv.apo_read_csv(csv_data)
-    apo_nips = set([ e['nip'] for e in ApoData['etuds'] ])
-    #
-    nips_ok = sem_nips.intersection(apo_nips)
-    nips_no_apo = sem_nips - apo_nips # dans ScoDoc mais pas dans Apogée
-    nips_no_sco = apo_nips - sem_nips # dans Apogée mais pas dans ScoDoc
-    
-    return nips_ok, apo_nips, nips_no_apo, nips_no_sco # ok si nips_no_apo et nips_no_sco sont vides
-
-def apo_csv_check(context, sem):
-    """
-    check students in stored maqs vs students in sem
-      Cas à détecter:
-      - étapes du semestre sans maquette CSV (etapes_missing_csv)
-      - etudiants ScoDoc sans code NIP (etuds_without_nip)
-      - etudiants dans sem (ScoDoc) mais dans aucun CSV (nips_no_apo)
-      - etudiants dans un CSV mais pas dans sem ScoDoc (nips_no_sco)
-      - etudiants dans plusieurs CSV (argh!)
-    """
-    formsemestre_id = sem['formsemestre_id']
-
-    # Etapes du semestre sans maquette CSV:
-    etapes_sem = apo_get_sem_etapes(context, sem)
-    etapes_apo = set(apo_csv_list_stored_etapes(context, sem))
-    etapes_missing_csv = etapes_sem - etapes_apo
-
-    # Etudiants inscrits dans ce semestre:
-    nt = context._getNotesCache().get_NotesTable(context, formsemestre_id)   
-    sem_etuds = nt.identdict.values()
-    sem_nips = set([ e['code_nip'] for e in sem_etuds if e['code_nip'] ])
-    
-    etuds_without_nip = [ e for e in sem_etuds if not e['code_nip'] ]
-        
-    # 
-    nips_ok = set()     # codes nip des etudiants dans ScoDoc et Apogée
-    nips_no_apo = sem_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
-    for etape_apo in etapes_apo:
-        et_nips_ok, et_apo_nips, et_nips_no_apo, et_nips_no_sco = apo_csv_check_etape(context, sem, sem_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)
-
-    # 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) 
-                      and (not etuds_without_nip) 
-                      and (not nips_no_apo)
-                      and (not apo_dups) )
-    
-    return ok_for_export, etapes_missing_csv, etuds_without_nip, nips_ok, nips_no_apo, nips_no_sco, apo_dups
-
-
-def apo_csv_retreive_etuds_by_nip(context, sem, 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(context, sem)
-    for etape_apo in etapes_apo:
-        csv_data = apo_csv_get(context, sem, etape_apo)
-        ApoData = sco_apogee_csv.apo_read_csv(csv_data)
-        etape_apo = ApoData['etape_apogee']
-        for e in ApoData['etuds']:
-            e['etape_apo'] = etape_apo
-        apo_etuds_by_nips.update( dict( [ (e['nip'], e) for e in ApoData['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 sco_sem_apogee import *
-import sco_groups
-import sco_groups_view
-import sco_formsemestre
-
-context = go_dept(app, 'RT')
-#sems = context.Notes.formsemestre_list()
-#sem = sems[0]
-#formsemestre_id = sem['formsemestre_id']
-formsemestre_id='SEM24910'
-sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
-
-csv_data = open('/opt/misc/VL4RT_V3ASR.TXT').read()
-
-
-print apo_csv_list_stored_archives(context, sem)
-
-apo_csv_store(context, REQUEST, sem, csv_data)
-
-groups_infos = sco_groups_view.DisplayedGroupsInfos(context, [sco_groups.get_default_group(context, formsemestre_id)], formsemestre_id=formsemestre_id, REQUEST=REQUEST)
-
-nt = context.Notes._getNotesCache().get_NotesTable(context.Notes, formsemestre_id)
-
-
-"""

Deleted: branches/ScoDoc7/sco_sem_apogee_view.py
===================================================================
--- branches/ScoDoc7/sco_sem_apogee_view.py	2016-06-26 21:48:03 UTC (rev 1509)
+++ branches/ScoDoc7/sco_sem_apogee_view.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -1,450 +0,0 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 2001 - 2016 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 at viennet.net
-#
-##############################################################################
-
-"""ScoDoc : formulaires gestion maquettes Apogee / export resultats
-"""
-
-from cStringIO import StringIO
-from zipfile import ZipFile
-
-from sco_utils import *
-from notes_log import log
-import sco_formsemestre
-import sco_formsemestre_status
-import notes_table
-from gen_tables import GenTable
-from sco_formsemestre_edit import can_edit_sem
-import sco_sem_apogee
-import sco_apogee_csv
-
-
-
-def apo_maq_status(context, formsemestre_id, REQUEST=None):
-    """Page statut / tableau de bord
-    
-    """
-    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
-    can_edit = can_edit_sem(context, REQUEST, formsemestre_id, sem=sem)
-    
-    tab_archives = table_apo_csv_list(context, formsemestre_id, REQUEST=REQUEST)
-
-    ok_for_export, etapes_missing_csv, etuds_without_nip, nips_ok, nips_no_apo, nips_no_sco, apo_dups = sco_sem_apogee.apo_csv_check(context, sem)
-    
-    
-    H = [ context.sco_header(REQUEST, page_title='Export Apogée' ),
-          '''<h2>Préparation export Apogée</h2>'''
-          '''<div class="apo_csv_1"><span>Etapes du semestre:</span> <b><tt>%s</tt></b>''' % sco_formsemestre_status.formsemestre_etape_apo_str(sem)
-    ]
-    if can_edit:
-        H.append( ''' (<a href="formsemestre_editwithmodules?formation_id=%(formation_id)s&formsemestre_id=%(formsemestre_id)s">modifier</a>)''' % sem )
-    H.append('</div>')
-    
-    # Maquettes enregistrées
-    H.append('<div class="apo_csv_list">')
-    if not tab_archives.is_empty():
-        H.append( tab_archives.html() )
-    else:
-        H.append( '''<p><em>Aucune maquette chargée</em></p>''' )
-    if can_edit:
-        H.append('''<form id="apo_csv_add" action="view_apo_csv_store" method="post" enctype="multipart/form-data">
-        <input type="file" size="30" name="csvfile"/>
-        <input type="hidden" name="formsemestre_id" value="%s"/>
-        <input type="submit" value="Ajouter ce fichier"/>
-        </form>''' % (formsemestre_id,)
-        )
-    H.append('</div>')    
-    
-    # Tableau de bord
-    if ok_for_export:
-        class_ok = 'apo_csv_status_ok'
-    else:
-        class_ok = 'apo_csv_status_nok'
-    
-    H.append('<div class="apo_csv_status %s">' % class_ok)
-    if ok_for_export:
-        H.append('<ul>')
-        if nips_no_sco: # seulement un warning
-            url_list = 'view_apo_etuds?formsemestre_id=%s&title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20ScoDoc:&nips=%s' % (formsemestre_id, '&nips='.join(nips_no_sco) )
-            H.append('<li class="apo_csv_warning">Attention: il y a <a href="%s">%d étudiant(s)</a> dans les maquettes Apogée chargées non inscrit(s) dans ce semestre ScoDoc;</li>' % (url_list, len(nips_no_sco)) )
-        
-        H.append('''<li>%d étudiants, prêt pour l'export.</li>'''%len(nips_ok))
-        H.append('<ul>')
-    else:
-        H.append('''<span>Problèmes à résoudre avant export des résultats:</span>''')
-        H.append('<div class="apo_csv_problems"><ul>')
-        
-        if etapes_missing_csv:
-            H.append('<li>Etapes sans maquette: <tt>%s</tt></li>' 
-                     % ', '.join(sorted(etapes_missing_csv)) )
-        if etuds_without_nip:
-            H.append('<li>%d étudiants ScoDoc sans code NIP</li>' % len(etuds_without_nip))
-        
-        if nips_no_apo:
-            url_list = 'view_scodoc_etuds?formsemestre_id=%s&title=Etudiants%%20ScoDoc%%20non%%20listés%%20dans%%20les%%20maquettes%%20Apogée%%20chargées&nips=%s' % (formsemestre_id, '&nips='.join(nips_no_apo) )
-            H.append('<li><a href="%s">%d étudiants</a> dans ce semestre non présents dans les maquettes Apogée chargées</li>' % (url_list, len(nips_no_apo)) )
-        
-        if nips_no_sco: # seulement un warning
-            url_list = 'view_apo_etuds?formsemestre_id=%s&title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20ScoDoc:&nips=%s' % (formsemestre_id, '&nips='.join(nips_no_sco) )
-            H.append('<li class="apo_csv_warning">Attention: il reste <a href="%s">%d étudiants</a> dans les maquettes Apogée chargées mais pas inscrits dans ce semestre ScoDoc</li>' % (url_list, len(nips_no_sco)) )
-
-        if apo_dups:
-            url_list = 'view_apo_etuds?formsemestre_id=%s&title=Doublons%%20Apogee&nips=%s' % (formsemestre_id, '&nips='.join(apo_dups) )
-            H.append('<li><a href="%s">%d étudiants</a> présents dans les <em>plusieurs</em> maquettes Apogée chargées</li>' % (url_list, len(apo_dups)) )
-        
-        H.append('</ul></div>')
-    H.append('</div>')
-
-    # Decisions de jury
-    nt = context._getNotesCache().get_NotesTable(context, formsemestre_id) 
-    jury_ok = nt.all_etuds_have_sem_decisions()
-
-    if jury_ok:
-        class_ok = 'apo_csv_jury_ok'
-    else:
-        class_ok = 'apo_csv_jury_nok'
-    H.append('<div class="apo_csv_jury %s">' % class_ok)
-    if jury_ok:
-         H.append('''<ul><li>Décisions de jury saisies</li></ul>''')
-         if ok_for_export:
-             H.append('''<form action="apo_csv_export_results" method="get">
-             <input type="submit" value="Export vers Apogée">
-             <input type="hidden" name="formsemestre_id" value="%s"/>
-             </form>''' % (formsemestre_id,))
-    else:
-        H.append('''<ul><li>Il manque des décisions de jury !</li></ul>''')
-    H.append('<div>')
-
-    # Aide:
-    H.append('''<div class="pas_help">
-    <h3>Explications</h3>
-    <p>Cette page permet de stocker les fichiers Apogée nécessaires pour 
-    l'export des résultats après les jurys, puis de remplir et exporter ces fichiers.
-    </p>
-    <p>
-    Les fichiers ("maquettes") Apogée sont de type CSV, du texte codé en %s.
-    </p>
-    <p>On a un fichier par étape Apogée. Pour les obtenir, se débrouiller avec 
-    Apogée. Leur contenu ressemble à cela:</p>
-    <pre class="small_pre_acc">
- 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 (...)
- 11681234	DUPONT	TOTO	 23/09/1986	18	20	ADM	18	20	ADM	(...)
-    </pre>
-    <p>Après avoir obtenu les fichier, stockez les dans ScoDoc 
-    (bouton "Ajouter fichier" en haut de cette page. Après vérification, il va 
-    apparaitre dans une table. Vous pouvez supprimer ce fichier, ou en ajouter 
-    d'autres si votre semestre correspond à plusieurs étapes Apogée.
-    </p>
-    <p>ScoDoc vérifie que tous les étudiants du semestre sont mentionnés dans 
-    un fichier Apogée et que les étapes correspondent.</p>
-    <p>Lorsque c'est le cas, et que les décisions de jury sont saisies, 
-    un bouton "Export vers Apogée" apparait et vous pouvez exporter les résultats.
-    <p>
-    <p>Vous obtiendrez alors un fichier ZIP comprenant tous les fichiers nécessaires.
-    Certains de ces fichiers devront être importés dans Apogée.
-    </p>
-    </div>
-    ''' % (sco_apogee_csv.APO_ENCODING,) )
-    H.append( context.sco_footer(REQUEST) )
-    return '\n'.join(H)
-
-
-def table_apo_csv_list(context, formsemestre_id, REQUEST=None):
-    """Table des archives (triée par date d'archivage)
-    """
-    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
-    can_edit = can_edit_sem(context, REQUEST, formsemestre_id, sem=sem)
-    T = sco_sem_apogee.apo_csv_list_stored_archives(context, sem).values()
-    T.sort(key=lambda x: x['date'])
-    for t in T:
-        # Ajoute qq infos pour affichage:
-        csv_data = sco_sem_apogee.apo_csv_get(context, sem, t['etape_apo'])
-        ApoData = sco_apogee_csv.apo_read_csv(csv_data)
-        t['filename'] = ApoData['titres']['apoC_Fichier_Exp']
-        t['nb_etuds'] = len(ApoData['etuds'])    
-        t['date_str'] = t['date'].strftime('%d/%m/%Y à %H:%M')
-        view_link = 'view_apo_csv?formsemestre_id=%s&etape_apo=%s' % (formsemestre_id, t['etape_apo'])
-        t['_filename_target'] = view_link
-        t['_etape_apo_target'] = view_link
-        t['suppress'] = icontag('delete_small_img', border='0', alt='supprimer', title='Supprimer')
-        t['_suppress_target'] = 'view_apo_csv_delete?formsemestre_id=%s&etape_apo=%s' % (formsemestre_id, t['etape_apo'])
-        
-    columns_ids=['filename', 'etape_apo', 'date_str', 'nb_etuds']
-    if can_edit:
-        columns_ids = ['suppress'] + columns_ids
-    
-    tab = GenTable( 
-        titles={ 
-            'archive_id' : '',
-            'filename' : 'Fichier',
-            'etape_apo' : 'Etape',
-            'nb_etuds' : 'Nb étudiants',
-            'date_str' : 'Enregistré le',            
-        },
-        columns_ids=columns_ids,
-        rows = T,
-        html_class='gt_table table_leftalign apo_maq_list',
-        html_sortable=True,
-        #base_url = '%s?formsemestre_id=%s' % (REQUEST.URL0, formsemestre_id),
-        #caption='Maquettes enregistrées',
-        preferences=context.get_preferences(formsemestre_id))
-    
-    return tab
-
-def view_apo_etuds(context, formsemestre_id, title='', nips=[], 
-                   format='html', REQUEST=None):
-    """Table des étudiants Apogée par nips
-    """
-    if nips and type(nips) != type([]):
-        nips = [nips]
-    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
-    etuds = sco_sem_apogee.apo_csv_retreive_etuds_by_nip(context, sem, nips)
-    
-    return _view_etuds_page(context, formsemestre_id, title=title, etuds=etuds.values(), 
-                            keys = ('nip', 'etape_apo', 'nom', 'prenom'),
-                            format=format, REQUEST=REQUEST)
-
-def view_scodoc_etuds(context, formsemestre_id, title='', etudids=None, nips=None,
-                      format='html', REQUEST=None):
-    """Table des étudiants ScoDoc par nips ou etudids
-    """
-    if etudids is not None:
-        if type(etudids) != type([]):
-            etudids = [etudids]
-        etuds = [ context.getEtudInfo(etudid=etudid, filled=True)[0] for etudid in etudids ]
-    elif nips is not None:
-        if type(nips) != type([]):
-            nips = [nips]
-        etuds = [ context.getEtudInfo(code_nip=nip, filled=True)[0] for nip in nips ]
-    else:
-        raise ValueError('etudid or NIP must be specified')
-
-    for e in etuds:
-        tgt = 'ficheEtud?etudid=' + e['etudid']
-        e['_nom_target'] = tgt
-        e['_prenom_target'] = tgt
-        e['_nom_td_attrs'] = 'id="%s" class="etudinfo"' % (e['etudid'],)
-        e['_prenom_td_attrs'] = 'id="pre-%s" class="etudinfo"' % (e['etudid'],)
-    
-    return _view_etuds_page(context, formsemestre_id, title=title, etuds=etuds, 
-                            keys=('code_nip', 'nom', 'prenom'),
-                            format=format, REQUEST=REQUEST)
-
-
-def _view_etuds_page(context, formsemestre_id, title='', etuds=[], 
-                     keys=(),
-                     format='html', REQUEST=None):
-    # Tri les étudiants par nom:
-    if etuds:
-        etuds.sort( key=lambda x: (x['nom'], x['prenom']) )
-    
-    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
-    H = [ context.sco_header(REQUEST, page_title=title,
-                             init_qtip = True,
-                             javascripts=['js/etud_info.js'] ),
-          '<h2>%s</h2>' % title
-          ]
-    
-    tab = GenTable( 
-        titles={ 
-            'nip' : 'Code NIP',
-            'code_nip' : 'Code NIP',
-            'etape_apo' : 'Etape',
-            'nom' : 'Nom',
-            'prenom' : 'Prénom',
-        },
-        columns_ids=keys, 
-        rows = etuds,
-        html_sortable=True,
-        html_class='gt_table table_leftalign',
-        filename='students_apo',
-        preferences=context.get_preferences(formsemestre_id)
-    )
-    if format != 'html':
-        return tab.make_page(context, format=format, REQUEST=REQUEST)
-
-    H.append( tab.html() )
-    
-    return '\n'.join(H) + context.sco_footer(REQUEST) 
-
-
-def view_apo_csv_store(context, formsemestre_id, csvfile, REQUEST=None):
-    """Store CSV data
-    """
-    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
-    data = csvfile.read()
-    filename = csvfile.filename
-    sco_sem_apogee.apo_csv_store(context, REQUEST, sem, data)
-    return REQUEST.RESPONSE.redirect('apo_formsemestre_status?formsemestre_id=' + formsemestre_id)
-
-def view_apo_csv_delete(context, formsemestre_id, etape_apo, dialog_confirmed=False, REQUEST=None):
-    """Delete CSV file
-    """
-    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
-    # check permission
-    if not can_edit_sem(context, REQUEST, sem=sem):
-        raise AccessDenied('opération non autorisée pour %s' % str(REQUEST.AUTHENTICATED_USER))
-    
-    dest_url = "apo_formsemestre_status?formsemestre_id=" + formsemestre_id
-    if not dialog_confirmed:
-        return context.confirmDialog(
-            """<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
-               <p>La suppression sera définitive.</p>"""
-            % (etape_apo,),
-            dest_url="", REQUEST=REQUEST, cancel_url=dest_url, 
-            parameters={'formsemestre_id' : formsemestre_id, 'etape_apo' : etape_apo })
-    
-    sco_sem_apogee.apo_csv_delete(context, sem, etape_apo, REQUEST=REQUEST)
-    return REQUEST.RESPONSE.redirect(dest_url+'&head_message=Archive%20supprimée')
-
-
-def view_apo_csv(context, formsemestre_id, etape_apo, format='html', REQUEST=None):
-    """Visualise une maquette stockée
-    """
-    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
-    csv_data = sco_sem_apogee.apo_csv_get(context, sem, etape_apo)
-    ApoData = sco_apogee_csv.apo_read_csv(csv_data)
-
-    ok_for_export, etapes_missing_csv, etuds_without_nip, nips_ok, nips_no_apo, nips_no_sco, apo_dups = sco_sem_apogee.apo_csv_check(context, sem)
-    
-    H = [ context.sco_header(REQUEST, page_title='Maquette Apogée enregistrée pour %s' % etape_apo,
-                             init_qtip = True,
-                             javascripts=['js/etud_info.js'] ),
-          '<h2>Etudiants dans la maquette Apogée %s</h2>' % etape_apo
-          ]
-    # Infos générales
-    H.append("""
-    <div class="apo_csv_infos">
-    <div class="apo_csv_etape"><span>Code étape:</span><span>%(etape_apogee)s</span></div>
-    <div class="apo_csv_etape"><span>Indices semestres:</span><span>%(semestre_ids)s</span></div>
-    
-    </div>
-    """ % ApoData )
-    
-    # Liste des étudiants (sans les résultats pour le moment): TODO
-    etuds = ApoData['etuds']
-    if not etuds:
-        return '\n'.join(H) + '<p>Aucun étudiant</p>' + context.sco_footer(REQUEST)
-
-    # Ajout infos sur ScoDoc vs Apogee
-    for e in etuds:
-        e['in_scodoc'] = e['nip'] not in nips_no_sco
-        e['in_scodoc_str'] = { True: 'oui', False : 'non' }[e['in_scodoc']]
-        if e['in_scodoc']:
-            e['_in_scodoc_str_target'] = 'ficheEtud?code_nip=' + e['nip']
-            e.update( context.getEtudInfo(code_nip=e['nip'], filled=True)[0] )
-            e['_nom_td_attrs'] = 'id="%s" class="etudinfo"' % (e['etudid'],)
-            e['_prenom_td_attrs'] = 'id="pre-%s" class="etudinfo"' % (e['etudid'],)
-        else:
-            e['_css_row_class'] = 'apo_not_scodoc'
-        
-    # Construit la table:
-    tab = GenTable( 
-        titles={ 
-            'nip' : 'Code NIP',
-            'nom' : 'Nom',
-            'prenom' : 'Prénom',
-            'naissance' : 'Naissance',
-            'in_scodoc_str' : 'Inscrit dans ce semestre ScoDoc',
-        },
-        columns_ids=('nip', 'nom', 'prenom', 'naissance', 'in_scodoc_str'),
-        rows = etuds,
-        html_sortable=True,
-        html_class='gt_table table_leftalign apo_maq_table',
-        base_url = '%s?formsemestre_id=%s&etape_apo=%s' % (REQUEST.URL0, formsemestre_id, etape_apo),
-        filename='students_' + etape_apo,
-        caption='Etudiants Apogée en ' + etape_apo,
-        preferences=context.get_preferences(formsemestre_id)
-    )
-
-    if format != 'html':
-        return tab.make_page(context, format=format, REQUEST=REQUEST)
-
-    H += [
-        tab.html(),
-        '''<div><a href="apo_formsemestre_status?formsemestre_id=%s">Retour</a>    
-        </div>'''
-        % formsemestre_id,
-        context.sco_footer(REQUEST) 
-        ]
-    
-    return '\n'.join(H)
-
-
-def apo_csv_export_results(context, formsemestre_id, REQUEST=None):
-    """Remplit les fichies CSV archivés
-    et donne un ZIP avec tous les résultats.
-    """
-    # nota: on peut éventuellement exporter même si tout n'est pas ok
-    # mais le lien via le tableau de bord n'est pas actif
-    # Les fichiers ne sont pas stockés: pas besoin de permission particulière
-    
-    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)    
-    data = StringIO()
-    dest_zip = ZipFile( data, 'w' )
-
-    etapes_apo = sco_sem_apogee.apo_csv_list_stored_etapes(context, sem)
-    for etape_apo in etapes_apo:
-        apo_csv = sco_sem_apogee.apo_csv_get(context, sem, etape_apo)
-        sco_apogee_csv.formsemestre_export_to_apogee(
-            context, formsemestre_id, 
-            apo_csv,
-            dest_zip=dest_zip )
-    
-    basename = context.get_preference('DeptName') + sem['anneescolaire'] + sem['titre_num']
-    basename = sco_sem_apogee.ApoCSVArchive.sanitize_filename(unescape_html(basename))
-    
-    dest_zip.close()
-    size = data.tell()
-    content_type = 'application/zip'
-    REQUEST.RESPONSE.setHeader('content-disposition',
-                               'attachement; filename="%s.zip"' % basename  )
-    REQUEST.RESPONSE.setHeader('content-type', content_type)
-    REQUEST.RESPONSE.setHeader('content-length', size)
-    return data.getvalue()

Modified: branches/ScoDoc7/sco_semset.py
===================================================================
--- branches/ScoDoc7/sco_semset.py	2016-06-26 21:48:03 UTC (rev 1509)
+++ branches/ScoDoc7/sco_semset.py	2016-07-03 21:13:07 UTC (rev 1510)
@@ -43,12 +43,15 @@
 from notesdb import *
 from notes_log import log
 import sco_formsemestre
+import sco_formsemestre_status
+import sco_etape_apogee
+from gen_tables import GenTable
 
 
 _semset_editor = EditableTable(
     'notes_semset',
     'semset_id',
-    ('title', )
+    ('semset_id', 'title', 'annee_scolaire', 'sem_id')
     )
 
 semset_create = _semset_editor.create
@@ -58,7 +61,8 @@
 
 
 class SemSet(dict):
-    def __init__(self, context, semset_id=None, title=''):
+    def __init__(self, context, semset_id=None, 
+                 title='', annee_scolaire='', sem_id=''):
         """Load and init, or, if semset_id is not specified, create
         """
         self.semset_id = semset_id
@@ -70,13 +74,19 @@
             L = semset_list(cnx, args={ 'semset_id' : semset_id } )
             if not L:
                 raise ValueError('invalid semset_id %s' % semset_id)
-            self.title = L[0]['title']
+            self['title'] = L[0]['title']
+            self['annee_scolaire'] = L[0]['annee_scolaire']
+            self['sem_id'] = L[0]['sem_id']
             r = SimpleDictFetch(context, "SELECT formsemestre_id FROM notes_semset_formsemestre WHERE semset_id = %(semset_id)s",
                         { 'semset_id' : semset_id } )
             if r:
-                self.formsemestre_ids = [ x['formsemestre_id'] for x in r ]
-            else: # create a new empty set
-                self.semset_id = semset_create(cnx, { 'title' : title } )
+                self.formsemestre_ids = set([ x['formsemestre_id'] for x in r ])                
+        else: # create a new empty set
+            self.semset_id = semset_create( cnx, {
+                'title' : title, 'annee_scolaire' : annee_scolaire, 'sem_id' : sem_id 
+                } )
+            log('created new semset_id=%s' % self.semset_id )
+        self.load_sems()
 
     def delete(self):
         """delete"""
@@ -91,21 +101,36 @@
         """Load formsemestres"""
         self.sems = []
         for formsemestre_id in self.formsemestre_ids:
-            self.sems.append(sco_formsemestre.get_formsemestre(self, formsemestre_id))
-        return self.sems
+            self.sems.append(sco_formsemestre.get_formsemestre(self.context, formsemestre_id))
 
+        if self.sems:
+            self['date_debut'] = min( [sem['date_debut_iso'] for sem in self.sems] )
+            self['date_fin'] = max( [sem['date_fin_iso'] for sem in self.sems] )
+        else:
+            self['date_debut'] = ''
+            self['date_fin'] = ''
+        self['_title_target'] = 'semset_status?semset_id=%s' % self.semset_id
+        self['etapes'] = self.list_etapes()
+        self['semtitles'] = [ sem['titre_num'] for sem in self.sems ]
+        self['semtitles_str'] = ', '.join(self['semtitles'])
+        self['_semtitles_str_target'] = self['_title_target']
+        
     def add(self, formsemestre_id):
-        SimpleQuery(self.context, "INSERT INTO notes_semset_formsemestre (formsemestre_id, semset_id) VALUES (%(formsemestre_id)s, %(semset_id)s)",
-                    {'formsemestre_id' : formsemestre_id, 'semset_id' : semset_id})
+        SimpleQuery(self.context, 
+                    "INSERT INTO notes_semset_formsemestre (formsemestre_id, semset_id) VALUES (%(formsemestre_id)s, %(semset_id)s)",
+                    {'formsemestre_id' : formsemestre_id, 'semset_id' : self.semset_id})
         self.load_sems() # update our list
 
-    
+    def remove(self, formsemestre_id):
+        SimpleQuery(self.context, 
+                    "DELETE FROM notes_semset_formsemestre WHERE semset_id=%(semset_id)s AND formsemestre_id=%(formsemestre_id)s",
+                    {'formsemestre_id' : formsemestre_id, 'semset_id' : self.semset_id})
+        self.load_sems() # update our list
+        
     def annees_scolaires(self):
         """Les annees scolaires. e.g. [ 2015, 2016 ], ou le plus souvent, une seule: [2016]
         L'année scolaire est l'année de début du semestre (2015 pour 2015-2016)
         """
-        if not self.sems:
-            self.load_sems()
         annees = list(set( [ int(s['annee_debut']) for s in self.sems ] ))
         annees.sort()
         return annees
@@ -113,8 +138,6 @@
     def list_etapes(self):
         """listes des étapes apogee des semestres. e.g.  ['V1RT', 'V2RT' ]
         """
-        if not self.sems:
-            self.load_sems()
         ets = set()
         for s in self.sems:
             for k in ('etape_apo', 'etape_apo2', 'etape_apo3', 'etape_apo4'):
@@ -124,4 +147,132 @@
         etapes.sort()
         return etapes
 
+
+def get_semsets_list(context):
+    """Liste de tous les semsets
+    Trié par date_debut, le plus récent d'abord
+    """
+    cnx = context.GetDBConnexion()
+    L = []
+    for s in semset_list(cnx):        
+        L.append( SemSet(context, semset_id = s['semset_id']) )
+    L.sort(key=lambda s: s['date_debut'], reverse=True)
+    return L
+
+def do_semset_create(context, title='', annee_scolaire=None, sem_id=None, REQUEST=None):
+    """Create new setset"""
+    s = SemSet(context, title=title, annee_scolaire=annee_scolaire, sem_id=sem_id)
+    return REQUEST.RESPONSE.redirect('semset_page')
+
+def do_semset_add_sem(context, semset_id, formsemestre_id, REQUEST=None):
+    """Add a sem to a semset"""
+    s = SemSet(context, semset_id)
+    # check for valid formsemestre_id
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) #raise exc
+
+    s.add(formsemestre_id)
+
+    return REQUEST.RESPONSE.redirect('semset_status?semset_id=%s'%semset_id)
+
+def do_semset_remove_sem(context, semset_id, formsemestre_id, REQUEST=None):
+    """Add a sem to a semset"""
+    s = SemSet(context, semset_id)
+    
+    s.remove(formsemestre_id)
+    
+    return REQUEST.RESPONSE.redirect('semset_status?semset_id=%s'%semset_id)
+    
+# ----------------------------------------
+def semset_status(context, semset_id, REQUEST=None):
+    """Montre un semset
+    """
+    s = SemSet(context, semset_id)
+    for sem in s.sems:
+        sco_formsemestre_status.fill_formsemestre(context, sem, REQUEST)
+        ets = sco_etape_apogee.apo_get_sem_etapes(context, sem)
+        sem['etapes_apo_str'] = ', '.join(sorted(list(ets)))
         
+    menu_sem = """<p><select name="formsemestre_id">
+        <option value="NULL" selected>(semestre)</option>"""
+    for sem in sco_formsemestre.do_formsemestre_list(context):
+        menu_sem += """<option value="%(formsemestre_id)s">%(titreannee)s</option>\n""" % sem
+    menu_sem += """</select>"""
+        
+    page_title = 'Ensemble de semestres ' + s['title']
+    H = [ context.sco_header(REQUEST, page_title=page_title,
+                             init_qtip = True,
+                             javascripts=[] ),
+          '<h2>%s</h2>' % page_title          
+          ]
+    H.append('<p>Année(s) scolaire(s): %s</p>' % ', '.join([ str(x) for x in s.annees_scolaires()]))
+    H.append('<p>Etapes: <tt>%s</tt></p>' % ', '.join(s.list_etapes()))
+    H.append('''<h4>Semestres de l'ensemble:</h4><ul class="semset_listsems">''')
+    for sem in s.sems:
+        H.append('<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>' % sem)
+        H.append('<a class="stdlink" href="do_semset_remove_sem?semset_id=%s&formsemestre_id=%s">(supprimer)</a>' 
+                 % (semset_id, sem['formsemestre_id']))
+        H.append('<br/>etapes: %(etapes_apo_str)s, %(nbinscrits)s inscrits' % sem )
+        H.append('</li>')
+    H.append('<li><form action="do_semset_add_sem" method="post">Ajouter un semestre:')
+    H.append(menu_sem)
+    H.append('<input type="hidden" name="semset_id" value="%s"/>' % semset_id)
+    H.append('<input type="submit" name="ajouter"/>')
+    H.append('</form></li>')
+    H.append('</ul>')
+    return '\n'.join(H) + context.sco_footer(REQUEST) 
+        
+
+def semset_page(context, format='html', REQUEST=None):
+    """Page avec liste semsets:
+    Table avec : date_debut date_fin titre liste des semestres
+    """
+    
+    tab = GenTable( 
+        rows = get_semsets_list(context),
+        titles = { 
+            'annee_scolaire' : 'Année scolaire', 
+            'sem_id' : 'S',
+            'date_debut':'Début', 
+            'date_fin':'Fin', 
+            'title':'', 
+            'semtitles_str': 'semestres' 
+            },
+        columns_ids=['annee_scolaire', 'sem_id', 'date_debut', 'date_fin', 'title', 'semtitles_str' ],        
+        html_sortable=True,
+        html_class='gt_table table_leftalign',
+        filename='semsets',
+        preferences=context.get_preferences() )
+    if format != 'html':
+        return tab.make_page(context, format=format, REQUEST=REQUEST)
+    
+    page_title = 'Ensembles de semestres'
+    H = [ context.sco_header(REQUEST, page_title=page_title,
+                             init_qtip = True,
+                             javascripts=[] ),
+          '<h2>%s</h2>' % page_title
+          ]
+    H.append( tab.html() )
+
+    annee_courante = int(context.AnneeScolaire(REQUEST))
+    menu_annee = '\n'.join([ '<option value="%s">%s</option>' % (i,i) for i in range(2014,annee_courante+1) ])
+    
+    H.append("""
+    <div style="margin-top:20px;">
+    <h4>Création nouvel ensemble</h4>
+    <form method="POST" action="do_semset_create">
+    <select name="annee_scolaire">
+    <option value="NULL" selected>(année scolaire)</option>""")
+    H.append(menu_annee)
+    H.append("""</select>
+    <select name="sem_id">
+    <option value="1">1re période (S1, S3)</option>
+    <option value="2">2de période (S2, S4)</option>
+    <option value="0">non semestrialisée (LP, ...)</option>
+    </select>
+    <input type="text" name="title" size="32"/>
+    <input type="submit" value="Créer"/>
+    </form></div>
+    """)
+    return '\n'.join(H) + context.sco_footer(REQUEST) 
+
+


Plus d'informations sur la liste de diffusion Scodoc-devel