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

eviennet at lipn.univ-paris13.fr eviennet at lipn.univ-paris13.fr
Ven 8 Juil 14:07:15 CEST 2016


Une pièce jointe HTML a été nettoyée...
URL: <https://www-rt.iutv.univ-paris13.fr/pipermail/scodoc-devel/attachments/20160708/e3bf531b/attachment-0001.html>
-------------- section suivante --------------
Modified: branches/ScoDoc7/ZNotes.py
===================================================================
--- branches/ScoDoc7/ZNotes.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/ZNotes.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -84,7 +84,7 @@
 import sco_synchro_etuds
 import sco_archives
 import sco_apogee_csv
-import sco_sem_apogee_view
+import sco_etape_apogee_view
 import sco_semset
 
 from sco_pdf import PDFLOCK
@@ -2673,30 +2673,30 @@
     
     security.declareProtected(ScoView, 'formsemestre_get_archived_file')
     formsemestre_get_archived_file = sco_archives.formsemestre_get_archived_file
-
-    #security.declareProtected(ScoView, 'formsemestre_export_to_apogee_form')  XXX A CHANGER 2016
-    #formsemestre_export_to_apogee_form = sco_apogee_csv.formsemestre_export_to_apogee_form
     
-    security.declareProtected(ScoView, 'view_apo_csv')
-    view_apo_csv = sco_sem_apogee_view.view_apo_csv
+    security.declareProtected(ScoEditApo, 'view_apo_csv')
+    view_apo_csv = sco_etape_apogee_view.view_apo_csv
+    
+    security.declareProtected(ScoEditApo, 'view_apo_csv_store')
+    view_apo_csv_store = sco_etape_apogee_view.view_apo_csv_store
 
-    security.declareProtected(ScoView, 'view_apo_csv_store')
-    view_apo_csv_store = sco_sem_apogee_view.view_apo_csv_store
-
-    security.declareProtected(ScoView, 'view_apo_csv_delete')
-    view_apo_csv_delete = sco_sem_apogee_view.view_apo_csv_delete
+    security.declareProtected(ScoEditApo, 'view_apo_csv_download_and_store')
+    view_apo_csv_download_and_store = sco_etape_apogee_view.view_apo_csv_download_and_store
     
-    security.declareProtected(ScoView, 'view_scodoc_etuds')
-    view_scodoc_etuds = sco_sem_apogee_view.view_scodoc_etuds    
+    security.declareProtected(ScoEditApo, 'view_apo_csv_delete')
+    view_apo_csv_delete = sco_etape_apogee_view.view_apo_csv_delete
     
-    security.declareProtected(ScoView, 'view_apo_etuds')
-    view_apo_etuds = sco_sem_apogee_view.view_apo_etuds
+    security.declareProtected(ScoEditApo, 'view_scodoc_etuds')
+    view_scodoc_etuds = sco_etape_apogee_view.view_scodoc_etuds    
+    
+    security.declareProtected(ScoEditApo, 'view_apo_etuds')
+    view_apo_etuds = sco_etape_apogee_view.view_apo_etuds
      
-    #security.declareProtected(ScoView, 'apo_formsemestre_status') XXX A CHANGER 2016
-    # apo_formsemestre_status = sco_sem_apogee_view.apo_formsemestre_status
+    security.declareProtected(ScoEditApo, 'apo_semset_maq_status')
+    apo_semset_maq_status = sco_etape_apogee_view.apo_semset_maq_status
 
-    security.declareProtected(ScoView, 'apo_csv_export_results')
-    apo_csv_export_results = sco_sem_apogee_view.apo_csv_export_results
+    security.declareProtected(ScoEditApo, 'apo_csv_export_results')
+    apo_csv_export_results = sco_etape_apogee_view.apo_csv_export_results
 
     # sco_semset
     security.declareProtected(ScoEditApo, 'semset_status')

Modified: branches/ScoDoc7/ZScolar.py
===================================================================
--- branches/ScoDoc7/ZScolar.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/ZScolar.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -629,7 +629,7 @@
             </ul>
             """)
         #
-        if authuser.has_permission(ScoEditApo,self) and False:  # disabled during devel
+        if authuser.has_permission(ScoEditApo,self) and True:  # disabled during devel
             H.append("""<hr>
             <h3>Exports Apogée</h3>
             <ul>

Modified: branches/ScoDoc7/config/postupgrade-db.py
===================================================================
--- branches/ScoDoc7/config/postupgrade-db.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/config/postupgrade-db.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -416,6 +416,12 @@
     check_field( cnx, 'notes_modules', 'code_apogee',
                  [ 'alter table notes_modules add column code_apogee text UNIQUE',
                    ])
+    check_field( cnx, 'notes_formsemestre', 'elt_sem_apo',
+                 [ 'alter table notes_formsemestre add column elt_sem_apo text',
+                   ])
+    check_field( cnx, 'notes_formsemestre', 'elt_annee_apo',
+                 [ 'alter table notes_formsemestre add column elt_annee_apo text',
+                   ])
     # Classement admission
     check_field(cnx, 'admissions', 'classement',
                 ['alter table admissions add column classement integer default NULL',
@@ -427,6 +433,7 @@
     if list_constraint( cnx, constraint_name='notes_modules_code_apogee_key' ):
         log('dropping buggy constraint on notes_modules_code_apogee')
         cursor.execute("alter  table notes_modules drop CONSTRAINT notes_modules_code_apogee_key;")
+    
     # SemSet:
     check_table( cnx, 'notes_semset', [
         """CREATE TABLE notes_semset  (
@@ -441,6 +448,7 @@
     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/sco_apogee_csv.py
===================================================================
--- branches/ScoDoc7/sco_apogee_csv.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/sco_apogee_csv.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -116,15 +116,27 @@
         NAR : 'NAR'
         }.get(code, 'DEF')
 
+def  _apo_fmt_note(note):
+    "Formatte une note pour Apogée"
+    if not note:
+        return ''
+    try:
+        val = float(note)
+    except ValueError:
+        return ''
+    return '%3.2f' % val
 
 class ApoElt:
     """Definition d'un Element Apogee
     sur plusieurs colonnes du fichier CSV
     """
     def __init__(self, cols):
+        assert len(cols) > 0
         assert( len(set( [ c['Code'] for c in cols ])) == 1 ) # colonnes de meme code
+        assert( len(set( [ c['Type Objet'] for c in cols ])) == 1 ) # colonnes de meme type
         self.cols = cols
         self.code = cols[0]['Code']
+        self.type_objet = cols[0]['Type Objet']
     def append(self, col):
         self.cols.append(col)
     def __str__(self):
@@ -140,24 +152,24 @@
 ETUD_NON_INSCRIT = 'non_inscrit'
 
 
-class ApoEtud:
+class ApoEtud(dict):
     """Etudiant Apogee: 
     """
     def __init__(self, nip='', nom='', prenom='', naissance='', cols={}):
-        self.nip = nip
-        self.nom = nom
-        self.prenom = prenom
-        self.naissance = naissance
-        self.cols = cols # { col_id : value }
+        self['nip'] = nip
+        self['nom'] = nom
+        self['prenom'] = prenom
+        self['naissance'] = naissance
+        self.cols = cols # { col_id : value }  colid = 'apoL_c0001'
         self.new_cols = {} # { col_id : value to record in csv }
         self.etud = None # etud ScoDoc
         self.etat = None # ETUD_OK, ...
         self.is_NAR = False # set to True si NARé dans un semestre
         self.log = []
         self.has_logged_no_decision = False
-        
+    
     def lookup_scodoc(self, context, etape_formsemestre_ids):
-        etuds = context.getEtudInfo(code_nip=self.nip, filled=True)
+        etuds = context.getEtudInfo(code_nip=self['nip'], filled=True)
         if not etuds:
             # pas dans ScoDoc
             self.etud = None 
@@ -166,7 +178,7 @@
         else:
             self.etud = etuds[0]
             # cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape:
-            formsemestre_ids = set(s['formsemestre_id'] for s in e.etud['sems'])
+            formsemestre_ids = { s['formsemestre_id'] for s in self.etud['sems'] }
             self.in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
             if not self.in_formsemestre_ids:
                 self.log.append( "connu dans ScoDoc, mais pas inscrit dans un semestre de cette étape" )
@@ -178,31 +190,34 @@
         """Recherche les valeurs des éléments Apogée pour cet étudiant
         Set .new_cols
         """    
-        self.col_elts = {} # { code_elt : { N, B, J, R } }
+        self.col_elts = {} # {'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}
         if self.etat is None:
-            self.lookup_scodoc(context, apo_data.etape_sems)
+            self.lookup_scodoc(context, apo_data.etape_formsemestre_ids)
         if self.etat != ETUD_OK:
             self.new_cols = self.cols # etudiant inconnu, recopie les valeurs existantes dans Apo
         else:
-            sco_elts = {} # code : { N, B, J, R }
-            for col_id in apo_data.col_ids:
-                code = apo_data.col_descr[col_id].code
-                el = sco_elts.get(code, None)
+            sco_elts = {} # valeurs trouvées dans ScoDoc   code : { N, B, J, R }
+            for col_id in apo_data.col_ids[4:]:
+                code = apo_data.cols[col_id]['Code'] # 'V1RT'
+                el = sco_elts.get(code, None) # {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}
                 if el is None:                    
-                    for sem in apo_data.etape_sems:
+                    for sem in apo_data.sems_etape:
                         el = self.search_elt_in_sem(context, code, sem)
                         if el != None:
                             sco_elts[code] = el
-                            break              
+                            break
                 self.col_elts[code] = el
                 if el is None:
                     self.new_cols[col_id] = self.cols[col_id]
                 else:
-                    self.new_cols[col_id] = sco_elts[code][ apo_data.cols['Type Rés.'] ]
+                    self.new_cols[col_id] = sco_elts[code][ apo_data.cols[col_id]['Type Rés.'] ]
+            # recopie les 4 premieres colonnes (nom, ..., naissance):
+            for col_id in apo_data.col_ids[:4]:
+                self.new_cols[col_id] = self.cols[col_id]
     
     def unassociated_codes(self, apo_data):
         "list of apo elements for this student without a value in ScoDoc"
-        codes = set([ apo_data.col_descr[col_id].code for col_id in apo_data.col_ids ])        
+        codes = set([ apo_data.cols[col_id].code for col_id in apo_data.col_ids ])        
         return codes - set(sco_elts)
     
     def search_elt_in_sem(self, context, code, sem):
@@ -210,14 +225,16 @@
         VET code jury etape
         ELP élément pédagogique: UE, module
         Que faire des autres éléments, comme VRT1A, VRTW1 ?
-        => VRTW1: ajouter un code additionnel au semestre ("code élement semestre", elt_sem_apo)
-        => VRT1A: le même que le VET: ajouter au semestre ("code élement annuel", elt_annee_apo)
+        => VRTW1: code additionnel au semestre ("code élement semestre", elt_sem_apo)
+        => VRT1A: le même que le VET: ("code élement annuel", elt_annee_apo)
 
         :return:  dict with N, B, J, R keys, or None si elt non trouvé
         """
         etudid = self.etud['etudid']
         nt = context._getNotesCache().get_NotesTable(context, sem['formsemestre_id'])
-        decision = nt.get_etud_decision_sem(etud['etudid'])
+        if etudid not in nt.identdict:
+            return None # etudiant non inscrit dans ce semestre
+        decision = nt.get_etud_decision_sem(self.etud['etudid'])
         if not decision:
             # pas de decision de jury, on n'enregistre rien
             if not self.has_logged_no_decision:
@@ -240,14 +257,14 @@
         for ue_id in decisions_ue.keys():
             ue = context.do_ue_list( args={ 'ue_id' : ue_id } )[0]
             if ue['code_apogee'] == code:
-                ue_status = nt.get_etud_ue_status(etud['etudid'], ue_id)
+                ue_status = nt.get_etud_ue_status(etudid, ue_id)
                 code_decision_ue = decisions_ue[ue_id]['code']
                 return dict( N=_apo_fmt_note(ue_status['moy']), B=20, J='', 
                              R=code_scodoc_to_apo(code_decision_ue) )
         # Modules ?
         modimpls = nt.get_modimpls()
         for modimpl in modimpls:
-            if modimpl['code_apogee'] == code:
+            if modimpl['module']['code_apogee'] == code:
                 n = nt.get_etud_mod_moy(moduleimpl_id, etudid)
                 if n != 'NI':
                     return dict( N=_apo_fmt_note(n), B=20 )
@@ -270,8 +287,8 @@
     def setup(self, context):
         """Recherche semestres ScoDoc concernés
         """
-        self.etape_sems = comp_apo_sems(context, self.etape_apogee, self.annee_scolaire )
-        self.etape_formsemestre_ids = set([s['formsemestre_id'] for s in etape_sems])
+        self.sems_etape = comp_apo_sems(context, self.etape_apogee, self.annee_scolaire )
+        self.etape_formsemestre_ids = { s['formsemestre_id'] for s in self.sems_etape }
         
     def read_csv(self, data):
         if not data:
@@ -340,7 +357,7 @@
             line = line.strip(APO_NEWLINE)
             fs = line.split(APO_SEP)
             cols = {} # { col_id : value }
-            for i in range(4, len(fs)):
+            for i in range(len(fs)):
                 cols[self.col_ids[i]] = fs[i]
             L.append( ApoEtud( 
                 nip= fs[0], # id etudiant
@@ -353,11 +370,10 @@
         return L
     
     def get_etape_apogee(self):
-        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)')
+        for elt in self.apo_elts.values():
+            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
@@ -381,9 +397,9 @@
         """write apo CSV etuds on f
         """
         for e in self.etuds:
-            fs = [ e['nip'], e['nom'], e['prenom'], e['naissance'] ]
+            fs = [] #  e['nip'], e['nom'], e['prenom'], e['naissance'] ]
             for col_id in self.col_ids:
-                fs += e.new_cols[col_id]
+                fs.append(str(e.new_cols[col_id]))
             f.write(APO_SEP.join(fs) + APO_NEWLINE)
 
     def list_unknown_elements(self):
@@ -403,15 +419,18 @@
         CR = [] # tableau compte rendu des decisions
         for e in self.etuds:
             cr = { 
-                'NIP' : e.nip,
-                'nom' : e.nom, 
-                'prenom' : e.prenom, 
-                'etape' : e.col_elts[self.etape_apogee].get('R', ''),                
-                'etape_note' : e.col_elts[self.etape_apogee].get('N', ''),  
+                'NIP' : e['nip'],
+                'nom' : e['nom'], 
+                'prenom' : e['prenom'], 
                 'est_NAR' : e.is_NAR,
                 'commentaire' : '; '.join(e.log)
                 }
-
+            if e.col_elts:
+                cr['etape' ] = e.col_elts[self.etape_apogee].get('R', '')
+                cr['etape_note'] = e.col_elts[self.etape_apogee].get('N', '')
+            else:
+                cr['etape' ] = ''
+                cr['etape_note'] = ''
             CR.append(cr)
 
         columns_ids=[ 'NIP', 'nom', 'prenom' ]
@@ -508,6 +527,33 @@
     return sco_formsemestre.list_formsemestre_by_etape(context, etape_apo=etape_apogee, annee_scolaire=annee_scolaire)
 
 
+def nar_etuds_table(context, apo_data, NAR_Etuds):
+    """Liste les NAR -> excel table
+    """
+    code_etape = apo_data.etape_apogee
+    today = datetime.datetime.today().strftime( '%d/%m/%y' )
+    L = []
+    NAR_Etuds.sort(key=lambda k: k['nom'])
+    for e in NAR_Etuds:
+        L.append( {
+            'nom' : e['nom'], 
+            'prenom' : e['prenom'],
+            'c0' : '', 
+            'c1' : 'AD', 
+            'etape' : code_etape, 
+            'c3' : '', 'c4' : '', 'c5' : '', 'c6' : 'N', 'c7' : '', 'c8' : '', 
+            'NIP' : e['nip'], 'c10' : '', 'c11' : '', 'c12' : '', 'c13' : 'NAR - Jury', 
+            'date' : today } )
+
+    columns_ids=( 'nom', 'prenom', 'c0', 'c1', 'etape', 'c3', 'c4', 'c5', 'c6', 'c7', 'c8',
+                  'NIP', 'c10', 'c11', 'c12', 'c13', 
+                  'date' )
+    T = GenTable( columns_ids=columns_ids, titles=dict(zip(columns_ids,columns_ids)),
+                  rows=L,
+                  xls_sheet_name='NAR ScoDoc'
+                  )
+    return T.excel()
+
 def export_csv_to_apogee(context, apo_csv_data, dest_zip=None, REQUEST=None ):
     """Genere un fichier CSV Apogée 
     à partir d'un fichier CSV Apogée vide (ou partiellement rempli)
@@ -516,21 +562,21 @@
     sinon crée un zip et le publie
     """
     apo_data = ApoData(apo_csv_data)
-    apo_data.setup(context) # -> .etape_sems
+    apo_data.setup(context) # -> .sems_etape
 
     for e in apo_data.etuds:
-        e.lookup_scodoc(context, etape_sems)
+        e.lookup_scodoc(context, apo_data.etape_formsemestre_ids)
         e.associate_sco(context, apo_data)
 
     # Ré-écrit le fichier Apogée
     f = StringIO()
     apo_data.write_header(f)
-    apo_data.write_etuds(context, f)
+    apo_data.write_etuds(f)
 
     # Table des NAR:
     NAR_Etuds = [ e for e in apo_data.etuds if e.is_NAR ]
     if NAR_Etuds:
-        nar_xls = nar_etuds_table(context, ApoData, NAR_Etuds)
+        nar_xls = nar_etuds_table(context, apo_data, NAR_Etuds)
     else:
         nar_xls = None
 
@@ -541,7 +587,7 @@
     Apo_Non_ScoDoc_Inscrits = [ e for e in apo_data.etuds if e.etat == ETUD_NON_INSCRIT ]
     # CR table
     cr_table = apo_data.build_cr_table()
-    cr_xls = T.excel()
+    cr_xls = cr_table.excel()
     #
     filename = apo_data.titles['apoC_Fichier_Exp']
     basename, ext = os.path.splitext(filename)
@@ -553,14 +599,14 @@
     logf = StringIO()
     logf.write('export_to_apogee du %s\n\n' % time.ctime() )
     logf.write('Semestres ScoDoc sources:\n')
-    for sem in apo_data.etape_sems:
+    for sem in apo_data.sems_etape:
         logf.write('\t%(titremois)s\n' % sem )
         
     logf.write('\nEtudiants Apogee non trouves dans ScoDoc:\n' 
-               + '\n'.join( [ '%s\t%s\t%s' % (e.nip, e.nom, e.prenom) for e in Apo_Non_ScoDoc ] )
+               + '\n'.join( [ '%s\t%s\t%s' % (e['nip'], e['nom'], e['prenom']) for e in Apo_Non_ScoDoc ] )
                )
     logf.write('\nEtudiants Apogee non inscrits sur ScoDoc dans cette étape:\n' 
-               + '\n'.join( [ '%s\t%s\t%s' % (e.nip, e.nom, e.prenom) for e in Apo_Non_ScoDoc_Inscrits ] )
+               + '\n'.join( [ '%s\t%s\t%s' % (e['nip'], e['nom'], e['prenom']) for e in Apo_Non_ScoDoc_Inscrits ] )
                )
     
     logf.write('\n\nElements Apogee inconnus dans ces semestres ScoDoc:\n' 

Modified: branches/ScoDoc7/sco_etape_apogee.py
===================================================================
--- branches/ScoDoc7/sco_etape_apogee.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/sco_etape_apogee.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -47,23 +47,23 @@
    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
+   apo_csv_semset_check(context, semset)
+      check students in stored maqs vs students in sem
+      Cas à détecter:
+      - etudiants ScoDoc sans code NIP
+      - etudiants dans sem (ScoDoc) mais dans aucun CSV
+      - etudiants dans un CSV mais pas dans sem ScoDoc
+      - etudiants dans plusieurs CSV (argh!)
       
+      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, semset, set_nips, etape_apo)
+      check une etape
+      
 """
 
 from sco_utils import *
@@ -89,7 +89,7 @@
     
 #     return archive_id
 
-def apo_csv_store(context, REQUEST, csv_data, annee_scolaire, sem_id):
+def apo_csv_store(context, 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)
@@ -118,6 +118,7 @@
 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
@@ -125,7 +126,7 @@
         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) )
+        e = re.compile( r'[0-9]{4}-' + str(sem_id) )
         oids = [ x for x in oids if e.match(x) ]
     
     infos = [] # liste d'infos
@@ -141,33 +142,42 @@
                 'etape_apo' : etape_apo,
                 'date' : ApoCSVArchive.get_archive_date(archive_id)
                 } ) 
-    
+    infos.sort(key=lambda x: x['etape_apo'])
     return infos
     
 def apo_csv_list_stored_etapes(context, annee_scolaire, sem_id=None):
     """
-    :return: list of stored etapes [{}]
+    :return: list of stored etapes [ 'V1RT', ... ]
     """
-    return apo_csv_list_stored_archives(context, annee_scolaire=annee_scolaire, sem_id=sem_id)
+    infos = apo_csv_list_stored_archives(context, annee_scolaire=annee_scolaire, sem_id=sem_id)
+    return [ info['etape_apo'] for info in infos ]
 
 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=''):
+
+def apo_csv_get_archive(context, etape_apo, annee_scolaire='', sem_id=''):
+    """Get archive"""
+    stored_archives = apo_csv_list_stored_archives(context, annee_scolaire=annee_scolaire, sem_id=sem_id)
+    for info in stored_archives:
+        if info['etape_apo'] == etape_apo:
+            return info
+    return None
+
+def apo_csv_get(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
+    info = apo_csv_get_archive(context, etape_apo, annee_scolaire, sem_id)
+    if not info:
+        raise ScoValueError('Etape %s non enregistree (%s, %s)' % (etape_apo, annee_scolaire, sem_id))
+    archive_id = info['archive_id']
+    data = 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):
@@ -178,62 +188,57 @@
     
     :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])
+    etapes = { sem[key] 
+               for key in ('etape_apo', 'etape_apo2', 'etape_apo3', 'etape_apo4')
+               if sem[key] 
+               }
     return etapes
 
-def apo_csv_check_etape(context, sem, sem_nips, etape_apo):
-    """Check etape
+def apo_csv_check_etape(context, semset, set_nips, etape_apo): 
+    """Check etape vs set of sems
     """
     # 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'] ])
+    csv_data = apo_csv_get(context, etape_apo, semset['annee_scolaire'], semset['sem_id'])
+    apo_data = sco_apogee_csv.ApoData(csv_data)
+    apo_nips = { e['nip'] for e in apo_data.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
+    nips_ok = set_nips.intersection(apo_nips)
+    nips_no_apo = set_nips - apo_nips # dans ScoDoc mais pas dans cette maquette Apogée
+    nips_no_sco = apo_nips - set_nips # dans Apogée mais pas dans ScoDoc
     
-    return nips_ok, apo_nips, nips_no_apo, nips_no_sco # ok si nips_no_apo et nips_no_sco sont vides
+    return nips_ok, apo_nips, nips_no_apo, nips_no_sco 
 
-def apo_csv_check(context, sem):
+def apo_csv_semset_check(context, semset): # was apo_csv_check
     """
-    check students in stored maqs vs students in sem
+    check students in stored maqs vs students in semset
       Cas à détecter:
-      - étapes du semestre sans maquette CSV (etapes_missing_csv)
+      - étapes 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 semset (ScoDoc) mais dans aucun CSV (nips_no_apo)
+      - etudiants dans un CSV mais pas dans semset ScoDoc (nips_no_sco)
       - etudiants dans plusieurs CSV (argh!)
     """
-    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
+    etapes_set = set(semset.list_etapes())
+    etapes_apo = set(apo_csv_list_stored_etapes(context, semset['annee_scolaire'], semset['sem_id']))
+    etapes_missing_csv = etapes_set - 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'] ]
-        
+    # Etudiants inscrits dans ce semset:
+    semset.load_etuds()
+
+    set_nips = set().union( *[ s['nips'] for s in semset.sems ] )
     # 
     nips_ok = set()     # codes nip des etudiants dans ScoDoc et Apogée
-    nips_no_apo = sem_nips.copy() # dans ScoDoc mais pas dans Apogée
+    nips_no_apo = set_nips.copy() # dans ScoDoc mais pas dans Apogée
     nips_no_sco = set() # dans Apogée mais pas dans ScoDoc
     etapes_apo_nips = [] # liste des nip de chaque maquette
     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)
+        et_nips_ok, et_apo_nips, et_nips_no_apo, et_nips_no_sco = apo_csv_check_etape(context, semset, set_nips, etape_apo)
         nips_ok |= et_nips_ok
         nips_no_apo -= et_apo_nips
         nips_no_sco |= et_nips_no_sco
         etapes_apo_nips.append(et_apo_nips)
-
+    
     # doublons: etudiants mentionnés dans plusieurs maquettes Apogée:
     apo_dups = set()
     if len(etapes_apo_nips) > 1:
@@ -244,28 +249,28 @@
     
     # All ok ?
     ok_for_export = ( (not etapes_missing_csv) 
-                      and (not etuds_without_nip) 
+                      and (not semset['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
+    return ok_for_export, etapes_missing_csv, semset['etuds_without_nip'], nips_ok, nips_no_apo, nips_no_sco, apo_dups
 
 
-def apo_csv_retreive_etuds_by_nip(context, sem, nips):
+def apo_csv_retreive_etuds_by_nip(context, semset, nips):
     """
     Search info about listed nips in stored CSV
     :return: list [ { 'etape_apo', 'nip', 'nom', 'prenom' } ]
     """
     apo_etuds_by_nips = {}
-    etapes_apo = apo_csv_list_stored_etapes(context, sem)
+    etapes_apo = apo_csv_list_stored_etapes(context, semset['annee_scolaire'], semset['sem_id'])
     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'] ] ) )
-
+        csv_data = apo_csv_get(context, etape_apo, semset['annee_scolaire'], semset['sem_id'])
+        apo_data = sco_apogee_csv.ApoData(csv_data)
+        etape_apo = apo_data.etape_apogee
+        for e in apo_data.etuds:
+            e.etape_apo = etape_apo
+        apo_etuds_by_nips.update( dict( [ (e['nip'], e) for e in apo_data.etuds ] ) )
+    
     etuds = {} # { nip : etud or None }
     for nip in nips:
         etuds[nip] = apo_etuds_by_nips.get(nip, { 'nip' : nip, 'etape_apo' : '?' })
@@ -277,23 +282,32 @@
 Tests:
 
 from debug import *
-from sco_sem_apogee import *
 import sco_groups
 import sco_groups_view
 import sco_formsemestre
 from sco_etape_apogee import *
+from sco_apogee_csv import *
+from sco_semset import *
 
-context = go_dept(app, 'RT')
+context = go_dept(app, 'RT').Notes
 #sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
 
-csv_data = open('/opt/misc/VL4RT_V3ASR.TXT').read()
-annee_scolaire=2016
+csv_data = open('/opt/misc/VDTRT_V1RT.TXT').read()
+annee_scolaire=2015
 sem_id=1
 
+apo_data = sco_apogee_csv.ApoData(csv_data)
+print apo_data.etape_apogee
+
+apo_data.setup(context)
+e = apo_data.etuds[0]
+e.lookup_scodoc(context, apo_data.etape_formsemestre_ids)
+e.associate_sco(context, apo_data)
+
 print apo_csv_list_stored_archives(context)
 
 
-apo_csv_store(context, REQUEST, csv_data, annee_scolaire, sem_id)
+apo_csv_store(context, csv_data, annee_scolaire, sem_id)
 
 
 
@@ -301,5 +315,14 @@
 
 nt = context.Notes._getNotesCache().get_NotesTable(context.Notes, formsemestre_id)
 
+#
+s = SemSet(context, 'NSS29245')
 
+
+# cas Tiziri KADDOUR (inscrite en S1, démission en fin de S1, pas inscrite en S2)
+#   export de S2: on voudrait DEF sur VRTW1, VRTW2 et sur V1RT
+
+apo_data.setup(context)
+e = apo_data.etuds[0]
+
 """

Modified: branches/ScoDoc7/sco_etape_apogee_view.py
===================================================================
--- branches/ScoDoc7/sco_etape_apogee_view.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/sco_etape_apogee_view.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -37,31 +37,33 @@
 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_semset
+import sco_etape_apogee
 import sco_apogee_csv
+import sco_portal_apogee
 
 
-
-def apo_maq_status(context, formsemestre_id, REQUEST=None):
+def apo_semset_maq_status(context, semset_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)
+    if not semset_id:
+        raise ValueError('invalid null semset_id')
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    semset.fill_formsemestres(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)
+    tab_archives = table_apo_csv_list( context, semset, REQUEST=REQUEST)
+
+    ok_for_export, etapes_missing_csv, etuds_without_nip, nips_ok, nips_no_apo, nips_no_sco, apo_dups = sco_etape_apogee.apo_csv_semset_check(context, semset)
+
+    ok_for_export &= semset['jury_ok']
     
-    
     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)
+          '''<div class="semset_descr">''',
+          semset.html_descr(),
+          '''</div>'''
     ]
-    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">')
@@ -69,13 +71,31 @@
         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">
+    # Upload fichier:
+    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="hidden" name="semset_id" value="%s"/>
         <input type="submit" value="Ajouter ce fichier"/>
-        </form>''' % (formsemestre_id,)
+        </form>''' % (semset_id,)
         )
+    # Récupération sur portail:
+    portal_url = sco_portal_apogee.get_portal_url(context)
+    if portal_url: # portail configuré
+        menu_etapes = '''<option value=""></option>'''
+        menu_etapes += ''.join(
+            [ '<option value="%s">%s</option>' % (et, et) 
+              for et in semset.list_etapes() 
+              ]) 
+        H.append('''<form id="apo_csv_download" action="view_apo_csv_download_and_store" method="post" enctype="multipart/form-data">
+        Ou récupérer maquette Apogée pour une étape:
+        <select name="etape_apo">
+        %s
+        </select>
+        <input type="hidden" name="semset_id" value="%s"/>
+        <input type="submit" value="Télécharger"/>
+        </form>''' % (menu_etapes, semset_id)
+        )
+    # 
     H.append('</div>')    
     
     # Tableau de bord
@@ -88,7 +108,7 @@
     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) )
+            url_list = 'view_apo_etuds?semset_id=%s&title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20ScoDoc:&nips=%s' % (semset_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))
@@ -104,38 +124,36 @@
             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) )
+            url_list = 'view_scodoc_etuds?semset_id=%s&title=Etudiants%%20ScoDoc%%20non%%20listés%%20dans%%20les%%20maquettes%%20Apogée%%20chargées&nips=%s' % (semset_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) )
+            url_list = 'view_apo_etuds?semset_id=%s&title=Etudiants%%20presents%%20dans%%20maquettes%%20Apogee%%20mais%%20pas%%20dans%%20ScoDoc:&nips=%s' % (semset_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) )
+            url_list = 'view_apo_etuds?semset_id=%s&title=Doublons%%20Apogee&nips=%s' % (semset_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)) )
-        
+            
+        if not semset['jury_ok']:
+            H.append('''<li>Il manque des décisions de jury !</li>''')
+            
         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:
+    if semset['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>''')
+    if semset and ok_for_export:
+        H.append('''<ul><li>Décisions de jury saisies</li></ul>''')
+        H.append('''<form action="apo_csv_export_results" method="get">
+            <input type="submit" value="Export vers Apogée">
+            <input type="hidden" name="semset_id" value="%s"/>
+            </form>''' % (semset_id,))
+    
     H.append('<div>')
 
     # Aide:
@@ -195,29 +213,30 @@
     return '\n'.join(H)
 
 
-def table_apo_csv_list(context, formsemestre_id, REQUEST=None):
+def table_apo_csv_list(context, semset, 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'])
+    annee_scolaire = semset['annee_scolaire']
+    sem_id = semset['sem_id']
+    
+    T = sco_etape_apogee.apo_csv_list_stored_archives(context, annee_scolaire, sem_id)
+    
     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'])    
+        csv_data = sco_etape_apogee.apo_csv_get(context, t['etape_apo'], annee_scolaire, sem_id)
+        apo_data = sco_apogee_csv.ApoData(csv_data)
+        t['filename'] = apo_data.titles['apoC_Fichier_Exp']
+        t['nb_etuds'] = len(apo_data.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'])
+        view_link = 'view_apo_csv?etape_apo=%s&semset_id=%s' % (t['etape_apo'], semset['semset_id'])
         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'])
+        t['_suppress_target'] = 'view_apo_csv_delete?etape_apo=%s&semset_id=%s' % (t['etape_apo'], semset['semset_id'])
         
     columns_ids=['filename', 'etape_apo', 'date_str', 'nb_etuds']
-    if can_edit:
-        columns_ids = ['suppress'] + columns_ids
+    #if can_edit:
+    columns_ids = ['suppress'] + columns_ids
     
     tab = GenTable( 
         titles={ 
@@ -233,27 +252,33 @@
         html_sortable=True,
         #base_url = '%s?formsemestre_id=%s' % (REQUEST.URL0, formsemestre_id),
         #caption='Maquettes enregistrées',
-        preferences=context.get_preferences(formsemestre_id))
+        preferences=context.get_preferences())
     
     return tab
 
-def view_apo_etuds(context, formsemestre_id, title='', nips=[], 
+def view_apo_etuds(context, semset_id, title='', nips=[], 
                    format='html', REQUEST=None):
     """Table des étudiants Apogée par nips
     """
+    if not semset_id:
+        raise ValueError('invalid null semset_id')
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    annee_scolaire = semset['annee_scolaire']
+    sem_id = semset['sem_id']
+    
     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)
+    etuds = sco_etape_apogee.apo_csv_retreive_etuds_by_nip(context, semset, nips)
     
-    return _view_etuds_page(context, formsemestre_id, title=title, etuds=etuds.values(), 
+    return _view_etuds_page(context, semset_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,
+def view_scodoc_etuds(context, semset_id, title='', etudids=None, nips=None,
                       format='html', REQUEST=None):
     """Table des étudiants ScoDoc par nips ou etudids
     """
+    raise NotImplementedError # TODO ?   XXX
     if etudids is not None:
         if type(etudids) != type([]):
             etudids = [etudids]
@@ -277,14 +302,13 @@
                             format=format, REQUEST=REQUEST)
 
 
-def _view_etuds_page(context, formsemestre_id, title='', etuds=[], 
+def _view_etuds_page(context, semset_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'] ),
@@ -304,7 +328,7 @@
         html_sortable=True,
         html_class='gt_table table_leftalign',
         filename='students_apo',
-        preferences=context.get_preferences(formsemestre_id)
+        preferences=context.get_preferences()
     )
     if format != 'html':
         return tab.make_page(context, format=format, REQUEST=REQUEST)
@@ -314,61 +338,87 @@
     return '\n'.join(H) + context.sco_footer(REQUEST) 
 
 
-def view_apo_csv_store(context, formsemestre_id, csvfile, REQUEST=None):
+def view_apo_csv_store(context, semset_id='', csvfile=None, data='', REQUEST=None):
     """Store CSV data
+    Le semset identifie l'annee scolaire et le semestre
+    Si csvfile, lit depuis FILE, sinon utilise 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)
+    if not semset_id:
+        raise ValueError('invalid null semset_id')
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
 
-def view_apo_csv_delete(context, formsemestre_id, etape_apo, dialog_confirmed=False, REQUEST=None):
+    if csvfile:
+        data = csvfile.read()
+    
+    if not data:
+        raise ScoValueError('view_apo_csv_store: no data')
+    
+    sco_etape_apogee.apo_csv_store(context, data, semset['annee_scolaire'], semset['sem_id'] )
+    
+    return REQUEST.RESPONSE.redirect('apo_semset_maq_status?semset_id=' + semset_id)
+
+def view_apo_csv_download_and_store(context, etape_apo='', semset_id='', REQUEST=None):
+    """Download maquette and store it
+    """
+    if not semset_id:
+        raise ValueError('invalid null semset_id')
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    
+    data = sco_portal_apogee.get_maquette_apogee(context, etape=etape_apo, annee_scolaire=semset['annee_scolaire'])
+    return view_apo_csv_store(context, semset_id, data=data, REQUEST=REQUEST)
+
+
+def view_apo_csv_delete(context, etape_apo='', semset_id='', 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 semset_id:
+        raise ValueError('invalid null semset_id')
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    dest_url = 'apo_semset_maq_status?semset_id=' + semset_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)
+            parameters={'semset_id' : semset_id, 'etape_apo' : etape_apo })
+
+    info = sco_etape_apogee.apo_csv_get_archive(context, etape_apo, 
+                                                semset['annee_scolaire'], semset['sem_id'])
+    sco_etape_apogee.apo_csv_delete(context, info['archive_id'])
     return REQUEST.RESPONSE.redirect(dest_url+'&head_message=Archive%20supprimée')
 
 
-def view_apo_csv(context, formsemestre_id, etape_apo, format='html', REQUEST=None):
+def view_apo_csv(context, etape_apo='', semset_id='', 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)
+    if not semset_id:
+        raise ValueError('invalid null semset_id')
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    annee_scolaire = semset['annee_scolaire']
+    sem_id = semset['sem_id']
+    csv_data = sco_etape_apogee.apo_csv_get(context, etape_apo, annee_scolaire, sem_id)
+    apo_data = sco_apogee_csv.ApoData(csv_data)
     
+    ok_for_export, etapes_missing_csv, etuds_without_nip, nips_ok, nips_no_apo, nips_no_sco, apo_dups = sco_etape_apogee.apo_csv_semset_check(context, semset)
+    
     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
+          '''<h2>Etudiants dans la maquette Apogée %s</h2>''' % etape_apo,
+          '''<p>Pour l'ensemble <a class="stdlink" href="semset_status?semset_id=%(semset_id)s">%(title)s</a></p>''' % semset
           ]
     # 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 class="apo_csv_etape"><span>Code étape:</span><span><tt>{0.etape_apogee}</tt> (année {0.annee_scolaire})</span></div>
+    <div class="apo_csv_etape"><span>Indice semestre:</span><span>{0.sem_id}</span></div>
     
     </div>
-    """ % ApoData )
+    """.format(apo_data) )
     
     # Liste des étudiants (sans les résultats pour le moment): TODO
-    etuds = ApoData['etuds']
+    etuds = apo_data.etuds
     if not etuds:
         return '\n'.join(H) + '<p>Aucun étudiant</p>' + context.sco_footer(REQUEST)
 
@@ -391,16 +441,16 @@
             'nom' : 'Nom',
             'prenom' : 'Prénom',
             'naissance' : 'Naissance',
-            'in_scodoc_str' : 'Inscrit dans ce semestre ScoDoc',
+            'in_scodoc_str' : 'Inscrit dans ces semestres 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),
+        base_url = '%s?etape_apo=%s&semset_id=%s' % (REQUEST.URL0, etape_apo, semset_id),
         filename='students_' + etape_apo,
         caption='Etudiants Apogée en ' + etape_apo,
-        preferences=context.get_preferences(formsemestre_id)
+        preferences=context.get_preferences()
     )
 
     if format != 'html':
@@ -408,37 +458,40 @@
 
     H += [
         tab.html(),
-        '''<div><a href="apo_formsemestre_status?formsemestre_id=%s">Retour</a>    
+        '''<div><a href="apo_semset_maq_status?semset_id=%s">Retour</a>    
         </div>'''
-        % formsemestre_id,
+        % semset_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
+def apo_csv_export_results(context, semset_id, REQUEST=None):
+    """Remplit les fichiers 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
+
+    if not semset_id:
+        raise ValueError('invalid null semset_id')
+    semset = sco_semset.SemSet(context, semset_id=semset_id)
+    annee_scolaire = semset['annee_scolaire']
+    sem_id = semset['sem_id']
     
-    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)
+    etapes_apo = sco_etape_apogee.apo_csv_list_stored_etapes(context, annee_scolaire, sem_id)
     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 )
+        apo_csv = sco_etape_apogee.apo_csv_get(context, etape_apo, annee_scolaire, sem_id)
+        sco_apogee_csv.export_csv_to_apogee( context, apo_csv,
+                                             dest_zip=dest_zip, REQUEST=REQUEST )
     
-    basename = context.get_preference('DeptName') + sem['anneescolaire'] + sem['titre_num']
-    basename = sco_sem_apogee.ApoCSVArchive.sanitize_filename(unescape_html(basename))
+    basename = context.get_preference('DeptName') + str(annee_scolaire) + '-%s-' % semset['sem_id'] + '-'.join(etapes_apo)
+    basename = sco_etape_apogee.ApoCSVArchive.sanitize_filename(unescape_html(basename))
     
     dest_zip.close()
     size = data.tell()

Modified: branches/ScoDoc7/sco_formsemestre.py
===================================================================
--- branches/ScoDoc7/sco_formsemestre.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/sco_formsemestre.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -44,7 +44,8 @@
      'etat', 'bul_hide_xml', 'bul_bgcolor',
      'etape_apo', 'etape_apo2', 'etape_apo3', 'etape_apo4',
      'modalite', 'resp_can_edit', 'resp_can_change_ens',
-     'ens_can_edit_eval'
+     'ens_can_edit_eval',
+     'elt_sem_apo', 'elt_annee_apo',
      ),
     sortkey = 'date_debut',
     output_formators = { 'date_debut' : DateISOtoDMY,

Modified: branches/ScoDoc7/sco_formsemestre_edit.py
===================================================================
--- branches/ScoDoc7/sco_formsemestre_edit.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/sco_formsemestre_edit.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -285,6 +285,16 @@
                         'title' : 'Code étape Apogée (4)',
                         'explanation' : '(si quatre étapes pour ce même semestre!)' })
         )
+    modform.append(
+        ('elt_sem_apo', { 'size' : 8,
+                          'title' : 'Element Apogée:',
+                          'explanation' : 'du semestre (ex: VRTW1)' })
+    )
+    modform.append(
+        ('elt_annee_apo', { 'size' : 8,
+                            'title' : 'Element Apogée:',
+                            'explanation' : "de l'année (ex: VRT1A)" })
+    )
     if edit:
         formtit = """
         <p><a href="formsemestre_edit_uecoefs?formsemestre_id=%s">Modifier les coefficients des UE capitalisées</a></p>

Modified: branches/ScoDoc7/sco_portal_apogee.py
===================================================================
--- branches/ScoDoc7/sco_portal_apogee.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/sco_portal_apogee.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -93,6 +93,18 @@
             else:
                 photo_url = portal_url + 'getPhoto.php'
         return photo_url
+
+    def get_maquette_url(self, context):
+        """Full URL of service giving Apogee maquette pour une étape (fichier "CSV")
+        """
+        maquette_url = context.get_preference('maquette_url')
+        if not maquette_url:
+            # Default:
+            portal_url = self.get_portal_url(context)
+            if not portal_url:
+                return None
+            maquette_url = portal_url + 'scodocMaquette.php'
+        return maquette_url
     
     def get_portal_api_version(self, context):
         "API version of the portal software"
@@ -107,6 +119,7 @@
 get_etapes_url = _PI.get_etapes_url
 get_etud_url = _PI.get_etud_url
 get_photo_url = _PI.get_photo_url
+get_maquette_url = _PI.get_maquette_url
 get_portal_api_version = _PI.get_portal_api_version
 
 def get_inscrits_etape(context, code_etape, anneeapogee=None):
@@ -403,3 +416,14 @@
                 etud['etape'] = None
 
     
+def get_maquette_apogee(context, etape='', annee_scolaire=''):
+    """Maquette CSV Apogee pour une étape et une annee scolaire
+    """
+    maquette_url = get_maquette_url(context)
+    if not maquette_url:
+        return None
+    portal_timeout = context.get_preference('portal_timeout')
+    req = maquette_url + '?' + urllib.urlencode((('etape', etape),('annee',annee_scolaire),))
+    doc = query_portal(req, timeout=portal_timeout)
+    return doc
+

Modified: branches/ScoDoc7/sco_preferences.py
===================================================================
--- branches/ScoDoc7/sco_preferences.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/sco_preferences.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -378,6 +378,15 @@
         'explanation' : "par defaut, selon l'api, getEtapes ou scodocEtapes sur l'URL du portail"
         }
       ),
+    ( 'maquette_url',
+      { 'initvalue' : '',
+        'title' : 'URL maquettes Apogee',
+        'size' : 40,
+        'category' : 'portal',
+        'only_global' : True,
+        'explanation' : "par defaut, scodocMaquette sur l'URL du portail"
+        }
+      ),
     ( 'portal_api',
       { 'initvalue' : 1,
         'title' : "Version de l'API",

Modified: branches/ScoDoc7/sco_semset.py
===================================================================
--- branches/ScoDoc7/sco_semset.py	2016-07-03 21:16:17 UTC (rev 1511)
+++ branches/ScoDoc7/sco_semset.py	2016-07-08 12:07:14 UTC (rev 1512)
@@ -66,6 +66,7 @@
         """Load and init, or, if semset_id is not specified, create
         """
         self.semset_id = semset_id
+        self['semset_id'] = semset_id
         self.context = context
         self.sems = [] 
         self.formsemestre_ids = []
@@ -80,7 +81,7 @@
             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 = set([ x['formsemestre_id'] for x in r ])                
+                self.formsemestre_ids = { x['formsemestre_id'] for x in r } # a set               
         else: # create a new empty set
             self.semset_id = semset_create( cnx, {
                 'title' : title, 'annee_scolaire' : annee_scolaire, 'sem_id' : sem_id 
@@ -114,8 +115,21 @@
         self['semtitles'] = [ sem['titre_num'] for sem in self.sems ]
         self['semtitles_str'] = ', '.join(self['semtitles'])
         self['_semtitles_str_target'] = self['_title_target']
+
+    def fill_formsemestres(self,REQUEST):
+        for sem in self.sems:
+            sco_formsemestre_status.fill_formsemestre(self.context, sem, REQUEST)
+            ets = sco_etape_apogee.apo_get_sem_etapes(self.context, sem)
+            sem['etapes_apo_str'] = ', '.join(sorted(list(ets)))
         
     def add(self, formsemestre_id):
+        # check
+        if formsemestre_id in self.formsemestre_ids:
+            return # already there
+        if formsemestre_id not in [ sem['formsemestre_id'] for sem in self.list_possible_sems() ]:
+            raise ValueError("can't add %s to set %s: incompatible sem_id"
+                             % (formsemestre_id, self.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})
@@ -147,7 +161,64 @@
         etapes.sort()
         return etapes
 
+    def list_possible_sems(self):
+        """List sems that can be added to this set
+        """
+        sems = sco_formsemestre.do_formsemestre_list(self.context)
+        # remove sems already here:
+        sems = [ sem for sem in sems if sem['formsemestre_id'] not in self.formsemestre_ids ]
+        # filter annee, sem_id:
+        if self['annee_scolaire']:
+            sems = [ sem for sem in sems 
+                     if sco_formsemestre.sem_in_annee_scolaire(
+                             self.context, sem, year=self['annee_scolaire']) 
+                    ]
+        if self['sem_id'] > 0:
+            sem_id2 = self['sem_id'] % 2
+            sems = [ sem for sem in sems 
+                     if sem['semestre_id'] > 0 and sem['semestre_id'] % 2 == sem_id2
+                     ]
+        return sems
+    
+    def load_etuds(self):
+        context = self.context
+        self['etuds_without_nip'] = set()
+        self['jury_ok'] = True
+        for sem in self.sems:
+            nt = context._getNotesCache().get_NotesTable(context, sem['formsemestre_id'])
+            sem['etuds'] = nt.identdict.values()
+            sem['nips'] = { e['code_nip'] for e in sem['etuds'] if e['code_nip'] }
+            sem['etuds_without_nip'] = { e for e in sem['etuds'] if not e['code_nip'] }
+            self['etuds_without_nip'] |= sem['etuds_without_nip']
+            sem['jury_ok'] = nt.all_etuds_have_sem_decisions()
+            self['jury_ok'] &= sem['jury_ok']
 
+    def html_descr(self):
+        """Short HTML description
+        """
+        H = [ '''<h3>Ensemble de semestres %(title)s</h3>''' % self ]
+        if self['annee_scolaire']:
+            H.append('<p>Année scolaire: %(annee_scolaire)s</p>' % self)
+        else:
+            H.append('<p>Année(s) scolaire(s) présentes: %s</p>' % ', '.join(
+                [ str(x) for x in self.annees_scolaires()]))
+        if self['sem_id']:
+            H.append('<p>Semestre: %(sem_id)s</p>' % self)
+        H.append('<p>Etapes: <tt>%s</tt></p>' % ', '.join(self.list_etapes()))
+        H.append('''<h4>Semestres de l'ensemble:</h4><ul class="semset_listsems">''')
+        for sem in self.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>' 
+                     % (self['semset_id'], sem['formsemestre_id']))
+            H.append('<br/>Etapes: <tt>%(etapes_apo_str)s</tt>, %(nbinscrits)s inscrits' % sem )
+            H.append('<br/>Elément Apogée année: <tt>%(elt_annee_apo)s</tt>' % sem )
+            H.append('<br/>Elément Apogée semestre: <tt>%(elt_sem_apo)s</tt>' % sem )
+            H.append('</br><em>vérifier les semestres antécédents !</em>')
+            H.append('</li>')
+        
+        return ''.join(H)
+
+
 def get_semsets_list(context):
     """Liste de tous les semsets
     Trié par date_debut, le plus récent d'abord
@@ -161,6 +232,8 @@
 
 def do_semset_create(context, title='', annee_scolaire=None, sem_id=None, REQUEST=None):
     """Create new setset"""
+    log('do_semset_create(title=%s, annee_scolaire=%s, sem_id=%s)' 
+        % (title, annee_scolaire, sem_id))
     s = SemSet(context, title=title, annee_scolaire=annee_scolaire, sem_id=sem_id)
     return REQUEST.RESPONSE.redirect('semset_page')
 
@@ -169,7 +242,7 @@
     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)
@@ -181,44 +254,46 @@
     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)))
+    s.fill_formsemestres(REQUEST)
         
+    possible_sems = s.list_possible_sems()
     menu_sem = """<p><select name="formsemestre_id">
-        <option value="NULL" selected>(semestre)</option>"""
-    for sem in sco_formsemestre.do_formsemestre_list(context):
+        <option value="" selected>(semestre)</option>"""
+    for sem in possible_sems:
         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 = [ 
+        context.sco_header(REQUEST, page_title=page_title, init_qtip = True, javascripts=[] ),
+        '''<div class="semset_descr">''',
+        s.html_descr(),
+        ]
+    
+    if possible_sems:
+        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" value="Ajouter"/>')
+        H.append('</form></li>')
+    else:
+        H.append('<li>pas de semestres à ajouter</li>')
     H.append('</ul>')
+    H.append('</div>') # fin description semset
+    
+    # Maquettes Apogee:
+    if s['annee_scolaire']:
+        H.append('''<p><a class="stdlink" href="apo_semset_maq_status?semset_id=%s">Maquettes Apogée associées</a></p>''' % (semset_id))
+        
+    H.append('''<p><a class="stdlink" href="semset_page">retour à la liste</a></p>''')
+    
     return '\n'.join(H) + context.sco_footer(REQUEST) 
         
 
@@ -261,7 +336,7 @@
     <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>""")
+    <option value="" selected>(année scolaire)</option>""")
     H.append(menu_annee)
     H.append("""</select>
     <select name="sem_id">


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