[Scodoc-devel] [SVN] Scolar : [1535] Formulaire saisie notes: nouvelle version entierement revue.

eviennet at lipn.univ-paris13.fr eviennet at lipn.univ-paris13.fr
Sam 23 Juil 17:41:14 CEST 2016


Une pièce jointe HTML a été nettoyée...
URL: <https://www-rt.iutv.univ-paris13.fr/pipermail/scodoc-devel/attachments/20160723/913a518b/attachment-0001.html>
-------------- section suivante --------------
Modified: branches/ScoDoc7/TrivialFormulator.py
===================================================================
--- branches/ScoDoc7/TrivialFormulator.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/TrivialFormulator.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -488,26 +488,37 @@
             R.append("""<script type="text/javascript">
             function enter_focus_next (elem, event) {
 		var cod = event.keyCode ? event.keyCode : event.which ? event.which : event.charCode;
-                enter = false;
-                if (event.keyCode == 13)
-                    enter = true;
-                if (event.which == 13)
-                    enter = true;
-                if (event.charCode == 13)
-                    enter = true;
+        var enter = false;
+        if (event.keyCode == 13)
+            enter = true;
+        if (event.which == 13)
+            enter = true;
+        if (event.charCode == 13)
+            enter = true;
 		if (enter) {
-			var i;
+            var focused = false;
+            var i;
 			for (i = 0; i < elem.form.elements.length; i++)
 				if (elem == elem.form.elements[i])
 					break;
-                        if (i < (elem.form.elements.length-3)) 
-                            elem.form.elements[i+1].focus();
+            i = i + 1;
+            while (i < elem.form.elements.length) {
+                if ((elem.form.elements[i].type == "text") && (!(elem.form.elements[i].disabled))) {
+                    elem.form.elements[i].focus();
+                    focused = true;
+                    break;
+                }
+                i = i + 1;
+            }
+            if (!focused) {
+                elem.blur();
+            }
 			return false;
 		} 
 		else
 		   return true;
 	}</script>
-            """) # enter_focus_next ignore 2 boutons a la fin (ok, cancel)
+            """) # enter_focus_next, ne focus que les champs text
         if suggest_js:
             # nota: formid is currently ignored 
             # => only one form with text_suggest field on a page.

Modified: branches/ScoDoc7/ZNotes.py
===================================================================
--- branches/ScoDoc7/ZNotes.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/ZNotes.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -1414,7 +1414,7 @@
         
         # ajoute infos sur enseignant:
         for ens in sem_ens:
-            sem_ens[ens].update(self.Users.user_info(ens,REQUEST))
+            sem_ens[ens].update(self.Users.user_info(ens))
             if sem_ens[ens]['email']:
                 sem_ens[ens]['_email_target'] = 'mailto:%s' % sem_ens[ens]['email']
         
@@ -2008,182 +2008,21 @@
     security.declareProtected(ScoEnsView, 'evaluation_edit')
     def evaluation_edit(self, evaluation_id, REQUEST ):
         "form edit evaluation"
-        return self.evaluation_create_form(evaluation_id=evaluation_id,
-                                           REQUEST=REQUEST,
-                                           edit=True)
+        return sco_evaluations.evaluation_create_form(
+            self,
+            evaluation_id=evaluation_id,
+            REQUEST=REQUEST,
+            edit=True)
+    
     security.declareProtected(ScoEnsView, 'evaluation_create')
     def evaluation_create(self, moduleimpl_id, REQUEST ):
         "form create evaluation"
-        return self.evaluation_create_form(moduleimpl_id=moduleimpl_id,
-                                           REQUEST=REQUEST,
-                                           edit=False)
-    
-    security.declareProtected(ScoEnsView, 'evaluation_create_form')
-    def evaluation_create_form(self, moduleimpl_id=None,
-                               evaluation_id=None,
-                               REQUEST=None,
-                               edit=False, readonly=False ):
-        "formulaire creation/edition des evaluations (pas des notes)"
-        if evaluation_id != None:
-            the_eval = self.do_evaluation_list( 
-                {'evaluation_id' : evaluation_id})[0]    
-            moduleimpl_id = the_eval['moduleimpl_id']
-        #
-        M = self.do_moduleimpl_list( args={ 'moduleimpl_id' : moduleimpl_id } )[0]
-        formsemestre_id = M['formsemestre_id']
-        if not readonly:
-            try:
-                self._evaluation_check_write_access( REQUEST,
-                                                     moduleimpl_id=moduleimpl_id )
-            except AccessDenied, detail:
-                return self.sco_header(REQUEST)\
-                       + '<h2>Opération non autorisée</h2><p>' + str(detail) + '</p>'\
-                       + '<p><a href="%s">Revenir</a></p>' % (str(REQUEST.HTTP_REFERER), ) \
-                       + self.sco_footer(REQUEST)
-        if readonly:
-            edit=True # montre les donnees existantes
-        if not edit:
-            # creation nouvel
-            if moduleimpl_id is None:
-                raise ValueError, 'missing moduleimpl_id parameter'
-            initvalues = { 'note_max' : 20,
-                           'jour' : time.strftime('%d/%m/%Y', time.localtime()) }
-            submitlabel = 'Créer cette évaluation'
-            action = 'Création d\'une é'
-            link=''
-        else:
-            # edition donnees existantes
-            # setup form init values
-            if evaluation_id is None:
-                raise ValueError, 'missing evaluation_id parameter'
-            initvalues = the_eval
-            moduleimpl_id = initvalues['moduleimpl_id']
-            submitlabel = 'Modifier les données'
-            if readonly:
-                action = 'E'
-                link='<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'%M['moduleimpl_id']
-            else:
-                action = 'Modification d\'une é'
-                link =''
-        #    
-        Mod = self.do_module_list( args={ 'module_id' : M['module_id'] } )[0]
-        sem = sco_formsemestre.get_formsemestre(self, M['formsemestre_id'])
-        #
-        help = """<div class="help"><p class="help">
-        Le coefficient d'une évaluation n'est utilisé que pour pondérer les évaluations au sein d'un module.
-        Il est fixé librement par l'enseignant pour refléter l'importance de ses différentes notes
-        (examens, projets, travaux pratiques...). Ce coefficient est utilisé pour calculer la note
-        moyenne de chaque étudiant dans ce module.
-        </p><p class="help">
-        Ne pas confondre ce coefficient avec le coefficient du module, qui est lui fixé par le programme
-        pédagogique (le PPN pour les DUT) et pondère les moyennes de chaque module pour obtenir
-        les moyennes d'UE et la moyenne générale.
-        </p><p class="help">
-        L'option <em>Visible sur bulletins</em> indique que la note sera reportée sur les bulletins
-        en version dite "intermédiaire" (dans cette version, on peut ne faire apparaitre que certaines
-        notes, en sus des moyennes de modules. Attention, cette option n'empêche pas la publication sur
-        les bulletins en version "longue" (la note est donc visible par les étudiants sur le portail).
-        </p><p class="help">
-        La modalité "rattrapage" permet de définir une évaluation dont les notes remplaceront les moyennes du modules
-        si elles sont meilleures que celles calculées. Dans ce cas, le coefficient est ignoré, et toutes les notes n'ont
-        pas besoin d'être rentrées.
-        </p>
-        """
-        mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (moduleimpl_id, Mod['code'], Mod['titre'], link)
-        if not readonly:
-            H = ['<h3>%svaluation en %s</h3>' % (action, mod_descr) ]
-        else:
-            E = initvalues
-            H = [ '<h3>Evaluation "%s"</h3><p><b>Module : %s</b></p>' % (E['description'], mod_descr) ]
-            # version affichage seule (générée ici pour etre plus jolie que le Formulator)
-            jour = E['jour']
-            if not jour:
-                jour = '<em>pas de date</em>'
-            H.append( '<p>Réalisée le <b>%s</b> de %s à %s '
-                      % (jour,E['heure_debut'],E['heure_fin']) )
-            if E['jour']:
-                group_id = sco_groups.get_default_group(self, formsemestre_id)
-                H.append('<span class="noprint"><a href="%s/Absences/EtatAbsencesDate?group_ids=%s&date=%s">(absences ce jour)</a></span>' % (self.ScoURL(),group_id,urllib.quote(E['jour'],safe='') ))
-            H.append( '</p><p>Coefficient dans le module: <b>%s</b> ' % E['coefficient'] )
-            if self.can_edit_notes(REQUEST.AUTHENTICATED_USER, moduleimpl_id, allow_ens=False):
-                H.append('<a href="evaluation_edit?evaluation_id=%s">(modifier l\'évaluation)</a>' % evaluation_id)
-            H.append('</p>')
-            return '<div class="eval_description">' + '\n'.join(H) + '</div>'
-
-        heures = [ '%02dh%02d' % (h,m) for h in range(8,19) for m in (0,30) ]
-        #
-        initvalues['visibulletin'] = initvalues.get('visibulletin', '1')
-        if initvalues['visibulletin'] == '1':            
-            initvalues['visibulletinlist'] = ['X']
-        else:
-            initvalues['visibulletinlist'] = []
-        if REQUEST.form.get('tf-submitted',False) and not REQUEST.form.has_key('visibulletinlist'):
-            REQUEST.form['visibulletinlist'] = []
-        #
-        tf = TrivialFormulator( REQUEST.URL0, REQUEST.form, (
-            ('evaluation_id', { 'default' : evaluation_id, 'input_type' : 'hidden' }),
-            ('formsemestre_id', { 'default' : formsemestre_id, 'input_type' : 'hidden' }),
-            ('moduleimpl_id', { 'default' : moduleimpl_id, 'input_type' : 'hidden' }),
-            #('jour', { 'title' : 'Date (j/m/a)', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }),
-            ('jour', { 'input_type' : 'date', 'title' : 'Date', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }),
-            ('heure_debut'   , { 'title' : 'Heure de début', 'explanation' : 'heure du début de l\'épreuve',
-                                 'input_type' : 'menu', 'allowed_values' : heures, 'labels' : heures }),
-            ('heure_fin'   , { 'title' : 'Heure de fin', 'explanation' : 'heure de fin de l\'épreuve',
-                               'input_type' : 'menu', 'allowed_values' : heures, 'labels' : heures }),
-            ('coefficient'    , { 'size' : 10, 'type' : 'float', 'explanation' : 'coef. dans le module (choisi librement par l\'enseignant)', 'allow_null':False }),
-        ('note_max'    , { 'size' : 3, 'type' : 'float', 'title' : 'Notes de 0 à', 'explanation' : 'barème', 'allow_null':False, 'max_value' : NOTES_MAX, 'min_value' : NOTES_PRECISION }),
-
-            ('description' , { 'size' : 36, 'type' : 'text', 'explanation' : 'type d\'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".' }),    
-            ('visibulletinlist', { 'input_type' : 'checkbox',
-                                   'allowed_values' : ['X'], 'labels' : [ '' ],
-                                   'title' : 'Visible sur bulletins' ,
-                                   'explanation' : '(pour les bulletins en version intermédiaire)'}),
-            ('publish_incomplete', { 'input_type' : 'boolcheckbox',
-                                     'title' : 'Prise en compte immédiate' ,
-                                     'explanation' : 'notes utilisées même si incomplètes'}),
-            ('evaluation_type', { 'input_type' : 'menu',
-                                  'title' : 'Modalité',
-                                  'allowed_values' : (EVALUATION_NORMALE, EVALUATION_RATTRAPAGE),
-                                  'type' : 'int',
-                                  'labels' : ('Normale', 'Rattrapage') }),
-            ), 
-                                cancelbutton = 'Annuler',
-                                submitlabel = submitlabel,
-                                initvalues = initvalues, readonly=readonly)
-
-        dest_url = 'moduleimpl_status?moduleimpl_id=%s' % M['moduleimpl_id']
-        if  tf[0] == 0:
-            return self.sco_header(REQUEST, init_jquery_ui=True) + '\n'.join(H) + '\n' + tf[1] + help + self.sco_footer(REQUEST)
-        elif tf[0] == -1:
-            return REQUEST.RESPONSE.redirect( dest_url )
-        else:
-            # form submission
-            if tf[2]['visibulletinlist']:
-                tf[2]['visibulletin'] = 1
-            else:
-                tf[2]['visibulletin'] = 0
-            if not edit:
-                # creation d'une evaluation
-                evaluation_id = self.do_evaluation_create( REQUEST, tf[2] )
-                return REQUEST.RESPONSE.redirect( dest_url )
-            else:
-                self.do_evaluation_edit( REQUEST, tf[2] )
-                return REQUEST.RESPONSE.redirect( dest_url )
-    
-    def _displayNote(self, val):
-        "convert note from DB to viewable string"
-        # Utilisé seulement pour I/O vers formulaires (sans perte de precision)
-        # Utiliser fmt_note pour les affichages
-        if val is None:
-            val = 'ABS'
-        elif val == NOTES_NEUTRALISE:
-            val = 'EXC' # excuse, note neutralise
-        elif val == NOTES_ATTENTE:
-            val = 'ATT' # attente, note neutralise
-        else:
-            val = '%g' % val
-        return val
-        
+        return sco_evaluations.evaluation_create_form(
+            self,
+            moduleimpl_id=moduleimpl_id,
+            REQUEST=REQUEST,
+            edit=False)
+            
     security.declareProtected(ScoView, 'evaluation_listenotes')
     def evaluation_listenotes(self, REQUEST=None ):
         """Affichage des notes d'une évaluation"""
@@ -2215,16 +2054,18 @@
     do_placement = sco_placement.do_placement
     
     # --- Saisie des notes    
-    security.declareProtected(ScoEnsView, 'notes_eval_selectetuds')
-    notes_eval_selectetuds = sco_saisie_notes.notes_eval_selectetuds
+    security.declareProtected(ScoEnsView, 'saisie_notes_tableur')
+    saisie_notes_tableur = sco_saisie_notes.saisie_notes_tableur
     
-    security.declareProtected(ScoEnsView, 'notes_evaluation_formnotes')
-    notes_evaluation_formnotes = sco_saisie_notes.evaluation_formnotes
-    
-    # now unused:
-    #security.declareProtected(ScoEnsView, 'do_evaluation_upload_csv')
-    #do_evaluation_upload_csv = sco_saisie_notes.do_evaluation_upload_csv
-    
+    security.declareProtected(ScoEnsView, 'feuille_saisie_notes')
+    feuille_saisie_notes = sco_saisie_notes.feuille_saisie_notes
+        
+    security.declareProtected(ScoEnsView, 'saisie_notes')
+    saisie_notes = sco_saisie_notes.saisie_notes
+
+    security.declareProtected(ScoEnsView, 'save_note')
+    save_note = sco_saisie_notes.save_note
+        
     security.declareProtected(ScoEnsView, 'do_evaluation_set_missing')
     do_evaluation_set_missing = sco_saisie_notes.do_evaluation_set_missing
 

Modified: branches/ScoDoc7/ZScoUsers.py
===================================================================
--- branches/ScoDoc7/ZScoUsers.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/ZScoUsers.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -239,10 +239,9 @@
         return [ r for r in roles if r and r[0] != '-' ]
 
     security.declareProtected(ScoUsersAdmin, 'user_info')
-    def user_info(self, user_name=None, user=None, REQUEST=None):        
+    def user_info(self, user_name=None, user=None):        
         """Donne infos sur l'utilisateur (qui peut ne pas etre dans notre base).
         Si user_name est specifie, interroge la BD. Sinon, user doit etre un dict.        
-        XXX REQUEST is not used
         """
         if user_name:
             infos = self._user_list( args={'user_name':user_name} )

Modified: branches/ScoDoc7/sco_evaluations.py
===================================================================
--- branches/ScoDoc7/sco_evaluations.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_evaluations.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -464,3 +464,185 @@
     # If requested, redirect to moduleimpl page:
     if redirect:
         return REQUEST.RESPONSE.redirect('moduleimpl_status?moduleimpl_id='+moduleimpl_id)
+
+
+#  -------------- VIEWS
+def evaluation_describe(context, evaluation_id='', edit_in_place=True, REQUEST=None):
+    """HTML description of evaluation, for page headers
+    edit_in_place: allow in-place editing when permitted (not implemented)
+    """
+    E = context.do_evaluation_list({'evaluation_id' : evaluation_id})[0]    
+    moduleimpl_id = E['moduleimpl_id']
+    M = context.do_moduleimpl_list( args={ 'moduleimpl_id' : moduleimpl_id } )[0]
+    Mod = context.do_module_list( args={ 'module_id' : M['module_id'] } )[0]
+    formsemestre_id = M['formsemestre_id']
+    sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
+    u = context.Users.user_info(M['responsable_id'])
+    resp = u['prenomnom']
+    nomcomplet = u['nomcomplet']
+    can_edit = context.can_edit_notes(REQUEST.AUTHENTICATED_USER, moduleimpl_id, allow_ens=False)
+
+    link='<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>' % moduleimpl_id
+    mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s' % (moduleimpl_id, Mod['code'], Mod['titre'], nomcomplet, resp, link)
+    
+    H = [ '<span class="eval_title">Evaluation "%s"</span><p><b>Module : %s</b></p>' % (E['description'], mod_descr) ]
+    jour = E['jour']
+    if not jour:
+        jour = '<em>pas de date</em>'
+    H.append( '<p>Réalisée le <b>%s</b> de %s à %s ' % (jour,E['heure_debut'],E['heure_fin']) )
+    if E['jour']:
+        group_id = sco_groups.get_default_group(context, formsemestre_id)
+        H.append('<span class="noprint"><a href="%s/Absences/EtatAbsencesDate?group_ids=%s&date=%s">(absences ce jour)</a></span>' % (context.ScoURL(),group_id,urllib.quote(E['jour'],safe='') ))
+    H.append( '</p><p>Coefficient dans le module: <b>%s</b> ' % E['coefficient'] )
+    if can_edit:
+        H.append('<a href="evaluation_edit?evaluation_id=%s">(modifier l\'évaluation)</a>' % evaluation_id)
+    H.append('</p>')
+    
+    return '<div class="eval_description">' + '\n'.join(H) + '</div>'
+
+
+def evaluation_create_form(
+        context, 
+        moduleimpl_id=None,
+        evaluation_id=None,
+        REQUEST=None,
+        edit=False, 
+        readonly=False,
+        page_title='Evaluation'
+        ):
+    "formulaire creation/edition des evaluations (pas des notes)"
+    if evaluation_id != None:
+        the_eval = context.do_evaluation_list( 
+            {'evaluation_id' : evaluation_id})[0]    
+        moduleimpl_id = the_eval['moduleimpl_id']
+    #
+    M = context.do_moduleimpl_list( args={ 'moduleimpl_id' : moduleimpl_id } )[0]
+    formsemestre_id = M['formsemestre_id']
+    if not readonly:
+        try:
+            context._evaluation_check_write_access( REQUEST,
+                                                 moduleimpl_id=moduleimpl_id )
+        except AccessDenied, detail:
+            return context.sco_header(REQUEST)\
+                   + '<h2>Opération non autorisée</h2><p>' + str(detail) + '</p>'\
+                   + '<p><a href="%s">Revenir</a></p>' % (str(REQUEST.HTTP_REFERER), ) \
+                   + context.sco_footer(REQUEST)
+    if readonly:
+        edit=True # montre les donnees existantes
+    if not edit:
+        # creation nouvel
+        if moduleimpl_id is None:
+            raise ValueError, 'missing moduleimpl_id parameter'
+        initvalues = { 'note_max' : 20,
+                       'jour' : time.strftime('%d/%m/%Y', time.localtime()) }
+        submitlabel = 'Créer cette évaluation'
+        action = 'Création d\'une é'
+        link=''
+    else:
+        # edition donnees existantes
+        # setup form init values
+        if evaluation_id is None:
+            raise ValueError, 'missing evaluation_id parameter'
+        initvalues = the_eval
+        moduleimpl_id = initvalues['moduleimpl_id']
+        submitlabel = 'Modifier les données'
+        if readonly:
+            action = 'E'
+            link='<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'%M['moduleimpl_id']
+        else:
+            action = 'Modification d\'une é'
+            link =''
+    #    
+    Mod = context.do_module_list( args={ 'module_id' : M['module_id'] } )[0]
+    sem = sco_formsemestre.get_formsemestre(context, M['formsemestre_id'])
+    #
+    help = """<div class="help"><p class="help">
+    Le coefficient d'une évaluation n'est utilisé que pour pondérer les évaluations au sein d'un module.
+    Il est fixé librement par l'enseignant pour refléter l'importance de ses différentes notes
+    (examens, projets, travaux pratiques...). Ce coefficient est utilisé pour calculer la note
+    moyenne de chaque étudiant dans ce module.
+    </p><p class="help">
+    Ne pas confondre ce coefficient avec le coefficient du module, qui est lui fixé par le programme
+    pédagogique (le PPN pour les DUT) et pondère les moyennes de chaque module pour obtenir
+    les moyennes d'UE et la moyenne générale.
+    </p><p class="help">
+    L'option <em>Visible sur bulletins</em> indique que la note sera reportée sur les bulletins
+    en version dite "intermédiaire" (dans cette version, on peut ne faire apparaitre que certaines
+    notes, en sus des moyennes de modules. Attention, cette option n'empêche pas la publication sur
+    les bulletins en version "longue" (la note est donc visible par les étudiants sur le portail).
+    </p><p class="help">
+    La modalité "rattrapage" permet de définir une évaluation dont les notes remplaceront les moyennes du modules
+    si elles sont meilleures que celles calculées. Dans ce cas, le coefficient est ignoré, et toutes les notes n'ont
+    pas besoin d'être rentrées.
+    </p>
+    """
+    mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (moduleimpl_id, Mod['code'], Mod['titre'], link)
+    if not readonly:
+        H = ['<h3>%svaluation en %s</h3>' % (action, mod_descr) ]
+    else:
+        return sco_evaluations.evaluation_describe(context, evaluation_id, REQUEST=REQUEST)            
+
+    heures = [ '%02dh%02d' % (h,m) for h in range(8,19) for m in (0,30) ]
+    #
+    initvalues['visibulletin'] = initvalues.get('visibulletin', '1')
+    if initvalues['visibulletin'] == '1':            
+        initvalues['visibulletinlist'] = ['X']
+    else:
+        initvalues['visibulletinlist'] = []
+    if REQUEST.form.get('tf-submitted',False) and not REQUEST.form.has_key('visibulletinlist'):
+        REQUEST.form['visibulletinlist'] = []
+    #
+    tf = TrivialFormulator( REQUEST.URL0, REQUEST.form, (
+        ('evaluation_id', { 'default' : evaluation_id, 'input_type' : 'hidden' }),
+        ('formsemestre_id', { 'default' : formsemestre_id, 'input_type' : 'hidden' }),
+        ('moduleimpl_id', { 'default' : moduleimpl_id, 'input_type' : 'hidden' }),
+        #('jour', { 'title' : 'Date (j/m/a)', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }),
+        ('jour', { 'input_type' : 'date', 'title' : 'Date', 'size' : 12, 'explanation' : 'date de l\'examen, devoir ou contrôle' }),
+        ('heure_debut'   , { 'title' : 'Heure de début', 'explanation' : 'heure du début de l\'épreuve',
+                             'input_type' : 'menu', 'allowed_values' : heures, 'labels' : heures }),
+        ('heure_fin'   , { 'title' : 'Heure de fin', 'explanation' : 'heure de fin de l\'épreuve',
+                           'input_type' : 'menu', 'allowed_values' : heures, 'labels' : heures }),
+        ('coefficient'    , { 'size' : 10, 'type' : 'float', 'explanation' : 'coef. dans le module (choisi librement par l\'enseignant)', 'allow_null':False }),
+    ('note_max'    , { 'size' : 3, 'type' : 'float', 'title' : 'Notes de 0 à', 'explanation' : 'barème', 'allow_null':False, 'max_value' : NOTES_MAX, 'min_value' : NOTES_PRECISION }),
+
+        ('description' , { 'size' : 36, 'type' : 'text', 'explanation' : 'type d\'évaluation, apparait sur le bulletins longs. Exemples: "contrôle court", "examen de TP", "examen final".' }),    
+        ('visibulletinlist', { 'input_type' : 'checkbox',
+                               'allowed_values' : ['X'], 'labels' : [ '' ],
+                               'title' : 'Visible sur bulletins' ,
+                               'explanation' : '(pour les bulletins en version intermédiaire)'}),
+        ('publish_incomplete', { 'input_type' : 'boolcheckbox',
+                                 'title' : 'Prise en compte immédiate' ,
+                                 'explanation' : 'notes utilisées même si incomplètes'}),
+        ('evaluation_type', { 'input_type' : 'menu',
+                              'title' : 'Modalité',
+                              'allowed_values' : (EVALUATION_NORMALE, EVALUATION_RATTRAPAGE),
+                              'type' : 'int',
+                              'labels' : ('Normale', 'Rattrapage') }),
+        ), 
+                            cancelbutton = 'Annuler',
+                            submitlabel = submitlabel,
+                            initvalues = initvalues, readonly=readonly)
+
+    dest_url = 'moduleimpl_status?moduleimpl_id=%s' % M['moduleimpl_id']
+    if  tf[0] == 0:
+        head = context.sco_header(
+            REQUEST, 
+            page_title=page_title,
+            init_jquery_ui=True)
+        return head + '\n'.join(H) + '\n' + tf[1] + help + context.sco_footer(REQUEST)
+    elif tf[0] == -1:
+        return REQUEST.RESPONSE.redirect( dest_url )
+    else:
+        # form submission
+        if tf[2]['visibulletinlist']:
+            tf[2]['visibulletin'] = 1
+        else:
+            tf[2]['visibulletin'] = 0
+        if not edit:
+            # creation d'une evaluation
+            evaluation_id = context.do_evaluation_create( REQUEST, tf[2] )
+            return REQUEST.RESPONSE.redirect( dest_url )
+        else:
+            context.do_evaluation_edit( REQUEST, tf[2] )
+            return REQUEST.RESPONSE.redirect( dest_url )
+

Modified: branches/ScoDoc7/sco_excel.py
===================================================================
--- branches/ScoDoc7/sco_excel.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_excel.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -40,6 +40,7 @@
 import time, datetime
 from types import StringType, IntType, FloatType, LongType
 
+# colors, voir exemple format.py
 COLOR_CODES = { 'black' : 0,
                 'red' : 0x0A,
                 'mauve' : 0x19,
@@ -244,7 +245,7 @@
 
 
 
-def Excel_feuille_saisie( E, description, lines ):
+def Excel_feuille_saisie( E, titreannee, description, lines ):
     """Genere feuille excel pour saisie des notes.
     E: evaluation (dict)
     lines: liste de tuples
@@ -254,10 +255,12 @@
     wb = Workbook()
     ws0 = wb.add_sheet(SheetName.decode(SCO_ENCODING))
     # ajuste largeurs colonnes (unite inconnue, empirique)
-    ws0.col(0).width = 3000 # codes
+    ws0.col(0).width =  400 # codes
     ws0.col(1).width = 6000 # noms
-    ws0.col(3).width = 1800 # groupes
-    ws0.col(5).width = 13000 # remarques
+    ws0.col(2).width = 4000 # prenoms
+    ws0.col(3).width = 6000 # groupes
+    ws0.col(4).width = 3000 # notes
+    ws0.col(5).width =13000 # remarques
     # styles
     style_titres = XFStyle()
     font0 = Font()
@@ -286,16 +289,16 @@
     style_ro = XFStyle() # cells read-only
     font_ro = Font()
     font_ro.name = 'Arial'
-    font_ro.colour_index = 0x19 # mauve, voir exemple format.py
+    font_ro.colour_index = COLOR_CODES['mauve']
     style_ro.font = font_ro
     style_ro.borders = rightborder
 
     style_dem = XFStyle() # cells read-only
     font_dem = Font()
     font_dem.name = 'Arial'
-    font_dem.colour_index = 0x3c # marron
+    font_dem.colour_index = COLOR_CODES['marron']
     style_dem.font = font_dem
-    style_dem.borders = topleftborders
+    style_dem.borders = topborders
     
     style = XFStyle()
     font1 = Font()
@@ -315,8 +318,16 @@
     style_notes.num_format_str = 'general'
     style_notes.pattern = Pattern() # fond jaune
     style_notes.pattern.pattern = Pattern.SOLID_PATTERN    
-    style_notes.pattern.pattern_fore_colour = 0x2b # jaune clair
+    style_notes.pattern.pattern_fore_colour = COLOR_CODES['lightyellow']
     style_notes.borders = topborders
+
+    style_comment = XFStyle()
+    font_comment = Font()
+    font_comment.name = 'Arial'
+    font_comment.height = 9*0x14
+    font_comment.colour_index = COLOR_CODES['blue']
+    style_comment.font = font_comment
+    style_comment.borders = topborders
     
     # ligne de titres
     li = 0
@@ -327,12 +338,16 @@
     li += 1
     ws0.write(li,0, u"Ne pas modifier les cases en mauve !",style_expl)
     li += 1
-    # description evaluation    
-    ws0.write(li,0, unescape_html(description), style_titres)
+    # Nom du semestre
+    ws0.write(li,0, unescape_html(titreannee).decode(SCO_ENCODING), style_titres)
     li += 1
+    # description evaluation
+    ws0.write(li,0, unescape_html(description).decode(SCO_ENCODING), style_titres)
+    li += 1
     ws0.write(li,0, u'Evaluation du %s (coef. %g)' % (E['jour'],E['coefficient']),
               style )
     li += 1
+    li += 1  # ligne blanche
     # code et titres colonnes
     ws0.write(li,0, u'!%s' % E['evaluation_id'], style_ro )
     ws0.write(li,1, u'Nom', style_titres )
@@ -361,7 +376,7 @@
         except:
             val = line[5].decode(SCO_ENCODING)
         ws0.write(li,4, val, style_notes ) # note
-        ws0.write(li,5, line[6].decode(SCO_ENCODING), st ) # comment
+        ws0.write(li,5, line[6].decode(SCO_ENCODING), style_comment ) # comment
     # explication en bas
     li+=2
     ws0.write(li, 1, u"Code notes", style_titres )

Modified: branches/ScoDoc7/sco_formsemestre_status.py
===================================================================
--- branches/ScoDoc7/sco_formsemestre_status.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_formsemestre_status.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -454,7 +454,7 @@
     
     inscrits = context.Notes.do_formsemestre_inscription_list( args={ 'formsemestre_id' : formsemestre_id } )
     sem['nbinscrits'] = len(inscrits)
-    u = context.Users.user_info(sem['responsable_id'],REQUEST)
+    u = context.Users.user_info(sem['responsable_id'])
     sem['resp'] = u['prenomnom']
     sem['nomcomplet'] = u['nomcomplet']
 
@@ -512,7 +512,7 @@
               'Module' : M['module']['abbrev'] or M['module']['titre'],
               '_Module_class' : 'scotext',
               'Inscrits' : len(ModInscrits),
-              'Responsable' : context.Users.user_info(M['responsable_id'],REQUEST)['nomprenom'],
+              'Responsable' : context.Users.user_info(M['responsable_id'])['nomprenom'],
               '_Responsable_class' : 'scotext',
               'Coef.' : M['module']['coefficient'],
               # 'ECTS' : M['module']['ects'],
@@ -706,9 +706,9 @@
     for M in Mlist:
         Mod = M['module']
         ModDescr = 'Module ' + M['module']['titre'] + ', coef. ' + str(M['module']['coefficient'])
-        ModEns = context.Users.user_info(M['responsable_id'],REQUEST)['nomcomplet']
+        ModEns = context.Users.user_info(M['responsable_id'])['nomcomplet']
         if M['ens']:
-            ModEns += ' (resp.), ' + ', '.join( [ context.Users.user_info(e['ens_id'],REQUEST)['nomcomplet'] for e in M['ens'] ] )
+            ModEns += ' (resp.), ' + ', '.join( [ context.Users.user_info(e['ens_id'])['nomcomplet'] for e in M['ens'] ] )
         ModInscrits = context.do_moduleimpl_inscription_list( args={ 'moduleimpl_id' : M['moduleimpl_id'] } )
         if prev_ue_id != M['ue']['ue_id']:
             prev_ue_id = M['ue']['ue_id']
@@ -745,7 +745,7 @@
                  % (M['moduleimpl_id'], ModDescr, Mod['abbrev'] or Mod['titre']))
         H.append('<td class="formsemestre_status_inscrits">%s</td>' % len( ModInscrits ))
         H.append('<td class="resp scotext"><a class="discretelink" href="moduleimpl_status?moduleimpl_id=%s" title="%s">%s</a></td>'
-                 % (M['moduleimpl_id'], ModEns, context.Users.user_info(M['responsable_id'],REQUEST)['prenomnom']))
+                 % (M['moduleimpl_id'], ModEns, context.Users.user_info(M['responsable_id'])['prenomnom']))
         
         H.append('<td class="evals">')
         nb_evals = etat['nb_evals_completes']+etat['nb_evals_en_cours']+etat['nb_evals_vides']

Modified: branches/ScoDoc7/sco_groups_view.py
===================================================================
--- branches/ScoDoc7/sco_groups_view.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_groups_view.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -125,7 +125,7 @@
     H.append( context.sco_footer(REQUEST) )
     return '\n'.join(H)
 
-def form_groups_choice(context, groups_infos):
+def form_groups_choice(context, groups_infos, with_selectall_butt=True):
     """form pour selection groupes
     group_ids est la liste des groupes actuellement sélectionnés
     et doit comporter au moins un élément, sauf si formsemestre_id est spécifié.
@@ -153,7 +153,8 @@
                          % (g['group_id'], selected, g['group_name'], n_members) )
         H.append('</optgroup>')
     H.append('</select> ')
-    H.append("""<input type="button" value="sélectionner tous" onmousedown="select_tous();"/>""")
+    if with_selectall_butt:
+        H.append("""<input type="button" value="sélectionner tous" onmousedown="select_tous();"/>""")
     H.append('</form>')
     H.append("""
     <script type="text/javascript">
@@ -274,6 +275,7 @@
                  group_ids=[], # groupes specifies dans l'URL
                  formsemestre_id=None, 
                  etat=None, 
+                 select_all_when_unspecified=False,
                  REQUEST=None):
         log('DisplayedGroupsInfos %s' % group_ids)
         if type(group_ids) == str:
@@ -281,18 +283,21 @@
                 group_ids = [group_ids] # cas ou un seul parametre, pas de liste
             else:
                 group_ids = []
-
+        
         if not group_ids: # appel sans groupe (eg page accueil)
             if not formsemestre_id:
                 raise Exception('missing parameter') # formsemestre_id or group_ids
-            # selectionne le premier groupe trouvé, s'il y en a un
-            partition = sco_groups.get_partitions_list(context, formsemestre_id, with_default=True)[0]
-            groups = sco_groups.get_partition_groups(context, partition)
-            if groups:
-                group_ids = [groups[0]['group_id']]
+            if select_all_when_unspecified:
+                group_ids = [ sco_groups.get_default_group(context, formsemestre_id) ]
             else:
-                group_ids = [ sco_groups.get_default_group(context, formsemestre_id) ]
-        
+                # selectionne le premier groupe trouvé, s'il y en a un
+                partition = sco_groups.get_partitions_list(context, formsemestre_id, with_default=True)[0]
+                groups = sco_groups.get_partition_groups(context, partition)
+                if groups:
+                    group_ids = [groups[0]['group_id']]
+                else:
+                    group_ids = [ sco_groups.get_default_group(context, formsemestre_id) ]
+
         self.base_url = REQUEST.URL0 + '?'
         gq = []
         for group_id in group_ids:

Modified: branches/ScoDoc7/sco_liste_notes.py
===================================================================
--- branches/ScoDoc7/sco_liste_notes.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_liste_notes.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -71,7 +71,7 @@
     
     # description de l'evaluation    
     if mode == 'eval':
-        H = [ context.evaluation_create_form(evaluation_id=evaluation_id, REQUEST=REQUEST, readonly=1) ]
+        H = [ sco_evaluations.evaluation_describe(context, evaluation_id=evaluation_id, REQUEST=REQUEST) ]
     else:
         H = []
     # groupes
@@ -359,7 +359,7 @@
         else:
             eval_info = '<span class="eval_info eval_incomplete">Notes incomplètes, évaluation non prise en compte dans les moyennes</span>'
             
-        return context.evaluation_create_form(evaluation_id=E['evaluation_id'], REQUEST=REQUEST, readonly=1) + eval_info + html_form + t + '\n'.join(C)
+        return sco_evaluations.evaluation_describe(context, evaluation_id=E['evaluation_id'], REQUEST=REQUEST) + eval_info + html_form + t + '\n'.join(C)
 
 
     
@@ -569,7 +569,7 @@
 
     if with_header:
         H = [ context.html_sem_header(REQUEST, "Vérification absences à l'évaluation"),
-              context.evaluation_create_form(evaluation_id=evaluation_id, REQUEST=REQUEST, readonly=1),
+              sco_evaluations.evaluation_describe(context, evaluation_id=evaluation_id, REQUEST=REQUEST),
               """<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>"""]
     else:
         # pas de header, mais un titre

Modified: branches/ScoDoc7/sco_moduleimpl_status.py
===================================================================
--- branches/ScoDoc7/sco_moduleimpl_status.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_moduleimpl_status.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -64,7 +64,7 @@
 
     menuEval = [
         { 'title' : 'Saisir notes',
-          'url' : 'notes_eval_selectetuds?evaluation_id=' + evaluation_id,
+          'url' : 'saisie_notes?evaluation_id=' + evaluation_id,
           'enabled' : context.can_edit_notes(REQUEST.AUTHENTICATED_USER, E['moduleimpl_id'])
           },
         { 'title' : 'Modifier évaluation',
@@ -124,7 +124,7 @@
 <table>
 <tr>
 <td class="fichetitre2">Responsable: </td><td class="redboldtext">""",
-    context.Users.user_info(M['responsable_id'],REQUEST)['nomprenom'],
+    context.Users.user_info(M['responsable_id'])['nomprenom'],
     """<span class="blacktt">(%(responsable_id)s)</span>""" % M,    
     ]
     try:
@@ -133,7 +133,7 @@
     except:
         pass
     H.append("""</td><td>""")
-    H.append(', '.join( [ context.Users.user_info(m['ens_id'],REQUEST)['nomprenom'] for m in M['ens'] ]))
+    H.append(', '.join( [ context.Users.user_info(m['ens_id'])['nomprenom'] for m in M['ens'] ]))
     H.append("""</td><td>""")
     try:
         context.can_change_ens(REQUEST, moduleimpl_id)
@@ -275,7 +275,7 @@
         if caneditevals:
             H.append("""<a class="smallbutton" href="evaluation_edit?evaluation_id=%s">%s</a>""" % (eval['evaluation_id'], icontag('edit_img', alt='modifier', title='Modifier informations')))
         if caneditnotes:
-            H.append("""<a class="smallbutton" href="notes_eval_selectetuds?evaluation_id=%s">%s</a>""" % (eval['evaluation_id'], icontag('notes_img', alt='saisie notes', title='Saisie des notes')))
+            H.append("""<a class="smallbutton" href="saisie_notes?evaluation_id=%s">%s</a>""" % (eval['evaluation_id'], icontag('notes_img', alt='saisie notes', title='Saisie des notes')))
         if etat['nb_notes'] == 0:
             if caneditevals:
                 H.append("""<a class="smallbutton" href="evaluation_delete?evaluation_id=%(evaluation_id)s">""" % eval)
@@ -310,7 +310,7 @@
         if etat['moy']:
             H.append( '%s / %g' % (etat['moy'], eval['note_max']))
         else:
-            H.append("""<a class="redlink" href="notes_eval_selectetuds?evaluation_id=%s">saisir notes</a>""" % (eval['evaluation_id']))
+            H.append("""<a class="redlink" href="saisie_notes?evaluation_id=%s">saisir notes</a>""" % (eval['evaluation_id']))
         H.append("""</td></tr>""")
         #
         if etat['nb_notes'] == 0:
@@ -337,13 +337,13 @@
                     if gr_moyenne['group_id'] in etat['gr_incomplets']:
                         H.append("""[<font color="red">""")
                         if caneditnotes:
-                            H.append("""<a class="redlink" href="notes_eval_selectetuds?evaluation_id=%s&group_ids:list=%s">incomplet</a></font>]""" % (eval['evaluation_id'], gr_moyenne['group_id']))
+                            H.append("""<a class="redlink" href="saisie_notes?evaluation_id=%s&group_ids:list=%s">incomplet</a></font>]""" % (eval['evaluation_id'], gr_moyenne['group_id']))
                         else:
                             H.append("""incomplet</font>]""")
                 else:
                     H.append("""<span class="redboldtext">  """)
                     if caneditnotes:
-                        H.append("""<a class="redlink" href="notes_eval_selectetuds?evaluation_id=%s&group_ids:list=%s">""" % (eval['evaluation_id'], gr_moyenne['group_id']))
+                        H.append("""<a class="redlink" href="saisie_notes?evaluation_id=%s&group_ids:list=%s">""" % (eval['evaluation_id'], gr_moyenne['group_id']))
                     H.append('pas de notes')
                     if caneditnotes:
                         H.append("""</a>""")

Modified: branches/ScoDoc7/sco_news.py
===================================================================
--- branches/ScoDoc7/sco_news.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_news.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -31,6 +31,7 @@
 import PyRSS2Gen
 from cStringIO import StringIO
 import datetime, re
+import time
 from stripogram import html2text, html2safehtml
 from email.MIMEMultipart import MIMEMultipart
 from email.MIMEText import MIMEText
@@ -71,13 +72,18 @@
 scolar_news_create = _scolar_news_editor.create
 scolar_news_list   = _scolar_news_editor.list
 
-def add(context, REQUEST, typ, object=None, text='', url=None ):
-    """Ajoute une nouvelle
+_LAST_NEWS = {} # { (authuser_name, type, object) : time }
+
+def add(context, REQUEST, typ, object=None, text='', url=None, max_frequency=False ):
+    """Ajoute une nouvelle.
+    Si max_frequency, ne genere pas 2 nouvelles identiques à moins de max_frequency
+    secondes d'intervalle.
     """
+    authuser_name=str(REQUEST.AUTHENTICATED_USER)
     cnx = context.GetDBConnexion()
     args = {
-        'authenticated_user' : str(REQUEST.AUTHENTICATED_USER),
-        'user_info' : context.Users.user_info(user_name=str(REQUEST.AUTHENTICATED_USER), REQUEST=REQUEST),
+        'authenticated_user' : authuser_name,
+        'user_info' : context.Users.user_info(user_name=authuser_name),
         'type' : typ,
         'object' : object,
         'text' : text,
@@ -85,26 +91,31 @@
         }
     
     log('news: %s' % args)
+    t = time.time()
+    if max_frequency:
+        last_news_time = _LAST_NEWS.get((authuser_name, typ, object), False)
+        if last_news_time and (t-last_news_time < max_frequency):
+            log('not recording')
+            return            
+    
+    _LAST_NEWS[ (authuser_name, typ, object) ] = t
+    
     _send_news_by_mail(context, args)
     return scolar_news_create(cnx,args,has_uniq_values=False)
 
-def resultset(cursor):
-    "generator"
-    row = cursor.dictfetchone()
-    while row:
-        yield row
-        row = cursor.dictfetchone()
         
 def scolar_news_summary(context, n=5):
     """Return last n news.
     News are "compressed", ie redondant events are joined.
     """
-    # XXX mauvais algo: oblige a extraire toutes les news pour faire le resume
+    
     cnx = context.GetDBConnexion()
     cursor = cnx.cursor(cursor_factory=ScoDocCursor)
-    cursor.execute( 'select * from scolar_news order by date asc' )
+    cursor.execute( 'select * from scolar_news order by date desc limit 100' )
     selected_news = {} # (type,object) : news dict
-    for r in resultset(cursor):
+    news = cursor.dictfetchall() # la plus récente d'abord
+    
+    for r in reversed(news): # la plus ancienne d'abord
         # si on a deja une news avec meme (type,object)
         # et du meme jour, on la remplace
         dmy = DateISOtoDMY(r['date']) # round
@@ -135,6 +146,7 @@
         infos = _get_formsemestre_infos_from_news(context, n)
         if infos:                        
             n['text'] += ' (<a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(descr_sem)s</a>)' % infos
+        n['text'] += ' par ' + context.Users.user_info(user_name=n['authenticated_user'])['nomcomplet']
     return news
 
 def _get_formsemestre_infos_from_news(context, n):

Modified: branches/ScoDoc7/sco_placement.py
===================================================================
--- branches/ScoDoc7/sco_placement.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_placement.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -37,6 +37,7 @@
 import scolars
 import sco_formsemestre
 import sco_groups
+import sco_evaluations
 import sco_excel
 from sco_excel import *
 from gen_tables import GenTable
@@ -61,8 +62,8 @@
     no_groups = (len(groups) == 1) and groups[0]['group_name'] is None
     
     # description de l'evaluation    
-    H = [ context.evaluation_create_form(
-        evaluation_id=evaluation_id, REQUEST=REQUEST, readonly=1),
+    H = [
+        sco_evaluations.evaluation_describe(context, evaluation_id=evaluation_id, REQUEST=REQUEST),
         '<h3>Placement et émargement des étudiants</h3>'
         ]
     #
@@ -143,7 +144,6 @@
         columns = tf[2]['columns']
         numbering = tf[2]['numbering']
         if columns in ('3', '4', '5', '6', '7', '8'):
-            # return notes_evaluation_formnotes( REQUEST )
             gs = [('group_ids%3Alist=' + urllib.quote_plus(x)) for x in group_ids ]
             query = 'evaluation_id=%s&placement_method=%s&teachers=%s&building=%s&room=%s&columns=%s&numbering=%s&' % (evaluation_id,placement_method,teachers,building,room,columns,numbering) + '&'.join(gs)
             return REQUEST.RESPONSE.redirect( REQUEST.URL1 + '/do_placement?' + query )

Modified: branches/ScoDoc7/sco_saisie_notes.py
===================================================================
--- branches/ScoDoc7/sco_saisie_notes.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_saisie_notes.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -26,6 +26,8 @@
 ##############################################################################
 
 """Saisie des notes
+
+   Formulaire revu en juillet 2016
 """
 import datetime
 
@@ -36,436 +38,91 @@
 from notes_table import *
 import sco_formsemestre
 import sco_groups
+import sco_groups_view
+from sco_formsemestre_status import makeMenu
 import sco_evaluations
+import sco_undo_notes
 import htmlutils
 import sco_excel
 import scolars
 import sco_news
 from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC
 
-def do_evaluation_selectetuds(context, REQUEST ):
+
+def convert_note_from_string(note, note_max, etudid=None, absents=[], tosuppress=[], invalids=[] ):
+    """converti une valeur (chaine saisie) vers une note numérique (float)
+    Les listes absents, tosuppress et invalids sont modifiées
     """
-    Choisi les etudiants pour saisie notes
-    """
-    evaluation_id = REQUEST.form['evaluation_id']
-    E = context.do_evaluation_list( {'evaluation_id' : evaluation_id})
-    if not E:
-        raise ScoValueError("invalid evaluation_id")
-    E = E[0]
-    M = context.do_moduleimpl_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
-    formsemestre_id = M['formsemestre_id']
-    # groupes
-    groups = sco_groups.do_evaluation_listegroupes(context,evaluation_id, include_default=True)
-    grlabs = [ g['group_name'] or 'tous' for g in groups ]  # legendes des boutons
-    grnams  = [ g['group_id'] for g in groups ] # noms des checkbox
-    no_groups = (len(groups) == 1) and groups[0]['group_name'] is None
-    
-    # description de l'evaluation    
-    H = [ context.evaluation_create_form(evaluation_id=evaluation_id,
-                                      REQUEST=REQUEST, readonly=1),
-          '<h3>Saisie des notes</h3>'
-          ]
-    #
-    descr = [
-        ('evaluation_id', { 'default' : evaluation_id, 'input_type' : 'hidden' }),
-        ('note_method', {'input_type' : 'radio', 'default' : 'form', 'allow_null' : False, 
-                         'allowed_values' : [ 'xls', 'form' ],
-                         'labels' : ['fichier tableur', 'formulaire web'],
-                         'title' : 'Méthode de saisie :' }) ]
-    if no_groups:
-        submitbuttonattributes = []
-        descr += [ 
-            ('group_ids', { 'default' : [g['group_id'] for g in groups],  'input_type' : 'hidden', 'type':'list' }) ]
+    invalid = False
+    note_value = None
+    note = note.replace(',','.')
+    if note[:3] == 'ABS':
+        note_value = None
+        absents.append(etudid)
+    elif note[:3] == 'NEU' or note[:3] == 'EXC':
+        note_value = NOTES_NEUTRALISE
+    elif  note[:3] == 'ATT':
+        note_value = NOTES_ATTENTE
+    elif note[:3] == 'SUP':
+        note_value = NOTES_SUPPRESS
+        tosuppress.append(etudid)
     else:
-        descr += [ 
-            ('group_ids', { 'input_type' : 'checkbox',
-                          'title':'Choix groupe(s) d\'étudiants :',
-                          'allowed_values' : grnams, 'labels' : grlabs,
-                          'attributes' : ['onchange="gr_change(this);"']
-                          }) ]
-        if not(REQUEST.form.has_key('group_ids') and REQUEST.form['group_ids']):
-            submitbuttonattributes = [ 'disabled="1"' ]
-        else:
-            submitbuttonattributes = [] # groupe(s) preselectionnés
-        H.append(
-          # JS pour desactiver le bouton OK si aucun groupe selectionné
-          """<script type="text/javascript">
-          function gr_change(e) {
-          var boxes = document.getElementsByName("group_ids:list");
-          var nbchecked = 0;
-          for (var i=0; i < boxes.length; i++) {
-              if (boxes[i].checked)
-                 nbchecked++;
-          }
-          if (nbchecked > 0) {
-              document.getElementsByName('gr_submit')[0].disabled=false;
-          } else {
-              document.getElementsByName('gr_submit')[0].disabled=true;
-          }
-          }
-          </script>
-          """
-          )
+        try:
+            note_value = float(note)
+            if (note_value < NOTES_MIN) or (note_value > note_max):
+                raise ValueError
+        except:
+            invalids.append(etudid)
+            invalid = True
 
-    tf = TrivialFormulator( REQUEST.URL0, REQUEST.form, descr,
-                            cancelbutton = 'Annuler',
-                            submitbuttonattributes=submitbuttonattributes,
-                            submitlabel = 'OK', formid='gr' )
-    if  tf[0] == 0:
-        H.append( """<div class="saisienote_etape1">
-        <span class="titredivsaisienote">Etape 1 : choix du groupe et de la méthode</span>
-        """)
-        return '\n'.join(H) + '\n' + tf[1] + "\n</div>"
-    elif tf[0] == -1:
-        return REQUEST.RESPONSE.redirect( '%s/Notes/moduleimpl_status?moduleimpl_id=%s'
-                                          % (context.ScoURL(),E['moduleimpl_id']) )
-    else:
-        # form submission
-        #   get checked groups
-        group_ids = tf[2]['group_ids']
-        note_method =  tf[2]['note_method']
-        if note_method in ('form', 'xls'):
-            # return notes_evaluation_formnotes( REQUEST )
-            gs = [('group_ids%3Alist=' + urllib.quote_plus(x)) for x in group_ids ]
-            query = 'evaluation_id=%s&note_method=%s&' % (evaluation_id,note_method) + '&'.join(gs)
-            REQUEST.RESPONSE.redirect( REQUEST.URL1 + '/notes_evaluation_formnotes?' + query )
-        else:
-            raise ValueError, "invalid note_method (%s)" % tf[2]['note_method'] 
+    return note_value, invalid
 
-def evaluation_formnotes(context, REQUEST ):
-    """Formulaire soumission notes pour une evaluation.
-    """
-    isFile = REQUEST.form.get('note_method','html') in ('csv','xls')
-    H = []
-    if not isFile:
-        H += [ context.sco_header(REQUEST, 
-                                  page_title='Saisie notes', 
-                                  init_qtip = True,
-                                  javascripts=['js/etud_info.js']),
-               "<h2>Saisie des notes</h2>" ]
-    
-    H += [do_evaluation_formnotes(context, REQUEST)]
-    if not isFile:
-        H += [ context.sco_footer(REQUEST) ]
-    
-    return ''.join(H)
 
-def do_evaluation_formnotes(context, REQUEST ):
-    """Formulaire soumission notes pour une evaluation.
-    parametres: evaluation_id, group_ids (liste des id de groupes)
+def _displayNote(val):
+    """Convert note from DB to viewable string.
+    Utilisé seulement pour I/O vers formulaires (sans perte de precision)
+    (Utiliser fmt_note pour les affichages)
     """
-    authuser = REQUEST.AUTHENTICATED_USER
-    authusername = str(authuser)
-    try:
-        evaluation_id = REQUEST.form['evaluation_id']
-    except:        
-        raise ScoValueError("Formulaire incomplet ! Vous avez sans doute attendu trop longtemps, veuillez vous reconnecter. Si le problème persiste, contacter l'administrateur. Merci.")
-    E = context.do_evaluation_list( {'evaluation_id' : evaluation_id})[0]
-    jour_iso = DateDMYtoISO(E['jour'])
-    # Check access
-    # (admin, respformation, and responsable_id)
-    if not context.can_edit_notes( authuser, E['moduleimpl_id'] ):
-        return '<h2>Modification des notes impossible pour %s</h2>' % authusername\
-               + """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
-               avez l'autorisation d'effectuer cette opération)</p>
-               <p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
-               """ % E['moduleimpl_id']
-           #
-    cnx = context.GetDBConnexion()
-    note_method = REQUEST.form['note_method']
-    okbefore = int(REQUEST.form.get('okbefore',0)) # etait ok a l'etape precedente
-    changed = int(REQUEST.form.get('changed',0)) # a ete modifie depuis verif 
-    #reviewed = int(REQUEST.form.get('reviewed',0)) # a ete presenté comme "pret a soumettre"
-    initvalues = {}
-    CSV = [] # une liste de liste de chaines: lignes du fichier CSV
-    CSV.append( ['Fichier de notes (à enregistrer au format CSV XXX)'])
-    # Construit liste des etudiants        
-    group_ids = REQUEST.form.get('group_ids', [] )
-    groups = sco_groups.listgroups(context, group_ids)
-    gr_title_filename = sco_groups.listgroups_filename(groups) 
-    gr_title = sco_groups.listgroups_abbrev(groups)
-
-    if None in [ g['group_name'] for g in groups ]: # tous les etudiants
-        getallstudents = True
-        gr_title = 'tous'
-        gr_title_filename = 'tous'
+    if val is None:
+        val = 'ABS'
+    elif val == NOTES_NEUTRALISE:
+        val = 'EXC' # excuse, note neutralise
+    elif val == NOTES_ATTENTE:
+        val = 'ATT' # attente, note neutralise
+    elif val == NOTES_SUPPRESS:
+        val = 'SUPR'
     else:
-        getallstudents = False
-    etudids = sco_groups.do_evaluation_listeetuds_groups(
-        context, evaluation_id, groups, getallstudents=getallstudents, include_dems=True)
-    if not etudids:
-        return '<p>Aucun groupe sélectionné !</p>'
-    # Notes existantes
-    NotesDB = context._notes_getall(evaluation_id)
-    #
-    M = context.do_moduleimpl_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
-    Mod = context.do_module_list( args={ 'module_id' : M['module_id'] } )[0]
-    sem = sco_formsemestre.get_formsemestre(context, M['formsemestre_id'])
-    evalname = '%s-%s' % (Mod['code'],DateDMYtoISO(E['jour']))
-    if E['description']:
-        evaltitre = '%s du %s' % (E['description'],E['jour'])
-    else:
-        evaltitre = 'évaluation du %s' % E['jour']
-    description = '%s: %s en %s (%s) resp. %s' % (sem['titreannee'], evaltitre, Mod['abbrev'], Mod['code'], strcapitalize(M['responsable_id']))
+        val = '%g' % val
+    return val
 
-    head = """
-    <h4>Codes spéciaux:</h4>
-    <ul>
-    <li>ABS: absent (compte comme un zéro)</li>
-    <li>EXC: excusé (note neutralisée)</li>
-    <li>SUPR: pour supprimer une note existante</li>
-    <li>ATT: note en attente (permet de publier une évaluation avec des notes manquantes)</li>
-    </ul>
-    <h3>%s</h3>
-    """ % description
-        
-    CSV.append ( [ description ] )
-    head += '<p>Etudiants des groupes %s (%d étudiants)</p>'%(gr_title,len(etudids))
-
-    head += '<em>%s</em> du %s (coef. %g, <span class="boldredmsg">notes sur %g</span>)' % (E['description'],E['jour'],E['coefficient'],E['note_max'])
-    CSV.append ( [ '', 'date', 'coef.' ] )
-    CSV.append ( [ '', '%s' % E['jour'], '%g' % E['coefficient'] ] )
-    CSV.append( ['!%s' % evaluation_id ] )
-    CSV.append( [ '', 'Nom', 'Prénom', 'Etat', 'Groupe',
-                  'Note sur %d'% E['note_max'], 'Remarque' ] )    
-
-    # JS code to monitor changes
-    head += """<script type="text/javascript">
-    function form_change() {
-    var cpar = document.getElementById('changepar');
-    // cpar.innerHTML += '*';
-    document.getElementById('tf').changed.value="1";
-    document.getElementById('tf').tf_submit.value = "Vérifier ces notes";
-    return true;
-    }        
-    </script>
-    <p id="changepar"></p>
+def _check_notes( notes, evaluation ):
+    """notes is a list of tuples (etudid, value)
+    returns list of valid notes (etudid, float value)
+    and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
     """
+    note_max = evaluation['note_max']
+    L = [] # liste (etudid, note) des notes ok (ou absent) 
+    invalids = [] # etudid avec notes invalides
+    withoutnotes = [] # etudid sans notes (champs vides)
+    absents = [] # etudid absents
+    tosuppress = [] # etudids avec ancienne note à supprimer
     
-    descr = [
-        ('evaluation_id', { 'default' : evaluation_id, 'input_type' : 'hidden' }),
-        ('group_ids', { 'default' : group_ids,  'input_type' : 'hidden', 'type':'list' }),
-        ('note_method', { 'default' : note_method, 'input_type' : 'hidden'}),
-        ('comment', { 'size' : 44, 'title' : 'Commentaire',
-                      'return_focus_next' : True, }),
-        ('changed', {'default':"0", 'input_type' : 'hidden'}), # changed in JS
-        ('s2' , {'input_type' : 'separator', 'title': '<br/>'}),
-        ]
-    el = [] # list de (label, etudid, note_value, explanation )
-    for etudid in etudids:
-        # infos identite etudiant (xxx sous-optimal: 1/select par etudiant)
-        ident = scolars.etudident_list(cnx, { 'etudid' : etudid })[0] # XXX utiliser ZScolar (parent)
-        # infos inscription
-        inscr = context.do_formsemestre_inscription_list(
-            {'etudid':etudid, 'formsemestre_id' : M['formsemestre_id']})[0]
-        nom = strupper(ident['nom'])
-        label = '%s %s' % (nom, strcapitalize(strlower(ident['prenom'])))
-        if NotesDB.has_key(etudid):
-            val = context._displayNote(NotesDB[etudid]['value'])
-            comment = NotesDB[etudid]['comment']
-            if comment is None:
-                comment = ''
-            explanation = '%s (%s) %s' % (NotesDB[etudid]['date'].strftime('%d/%m/%y %Hh%M'),
-                                          NotesDB[etudid]['uid'], comment )
+    for (etudid, note) in notes:
+        note = str(note).strip().upper()
+        if note[:3] == 'DEM':
+            continue # skip !
+        if note:
+            value, invalid = convert_note_from_string(
+                note, note_max, 
+                etudid=etudid, absents=absents, tosuppress=tosuppress, invalids=invalids )
+            if not invalid:
+                L.append((etudid,value))
         else:
-            explanation = ''
-            val = ''
-        # Information sur absence (ne tient pas compte de la demi-journée)
-        nbabs = context.Absences.CountAbs(etudid, jour_iso, jour_iso)
-        nbabsjust = context.Absences.CountAbsJust(etudid, jour_iso, jour_iso)
-        absinfo = ''
-        if nbabs:
-            if nbabsjust:
-                absinfo = 'absent justifié ce jour !  '
-            else:
-                absinfo = 'absent ce jour !  '
-        explanation = absinfo + explanation
-        #
-        el.append( (nom, label, etudid, val, explanation, ident, inscr) )
-    el.sort() # sort by name
-    for (nom, label, etudid, val, explanation, ident, inscr) in el:
+            withoutnotes.append(etudid)
+    
+    return L, invalids, withoutnotes, absents, tosuppress
 
-        if inscr['etat'] == 'D':
-            label = '<span class="etuddem">' + label + '</span>'
-            if not val:
-                val = 'DEM'
-                explanation = 'Démission'
-        initvalues['note_'+etudid] = val
 
-        label_link = '<a class="etudinfo" id="%s">%s</a>' % (etudid, label)
-        descr.append( ('note_'+etudid, { 'size' : 4, 'title' : label_link,
-                                         'explanation':explanation,
-                                         'return_focus_next' : True,
-                                         'attributes' : ['onchange="form_change();"'],
-                                         } ) )
-        groups = sco_groups.get_etud_groups(context, ident['etudid'], sem)
-        grc = sco_groups.listgroups_abbrev(groups)
-        CSV.append( [ '%s' % etudid, strupper(ident['nom']), strcapitalize(strlower(ident['prenom'])),
-                      inscr['etat'],
-                      grc, val, explanation ] )
-    if note_method == 'csv':
-        CSV = CSV_LINESEP.join( [ CSV_FIELDSEP.join(x) for x in CSV ] )
-        filename = 'notes_%s_%s.csv' % (evalname,gr_title_filename)
-        return sendCSVFile(REQUEST,CSV, filename )
-    elif note_method == 'xls':
-        filename = 'notes_%s_%s.xls' % (evalname, gr_title_filename)
-        xls = sco_excel.Excel_feuille_saisie( E, description, lines=CSV[6:] )
-        return sco_excel.sendExcelFile(REQUEST, xls, filename )
-
-    if REQUEST.form.has_key('changed'): # reset
-        del REQUEST.form['changed']
-    tf =  TF( REQUEST.URL0, REQUEST.form, descr, initvalues=initvalues,
-              cancelbutton='Annuler', submitlabel='Vérifier ces notes',
-              top_buttons = True
-              )
-    junk = tf.getform()  # check and init
-    if tf.canceled():
-        return REQUEST.RESPONSE.redirect( '%s/Notes/notes_eval_selectetuds?evaluation_id=%s'
-                                          % (context.ScoURL(), evaluation_id) )
-    elif (not tf.submitted()) or not tf.result:
-        # affiche premier formulaire
-        tf.formdescription.append(
-            ('okbefore', { 'input_type':'hidden', 'default' : 0 } ) )
-        form = tf.getform()            
-        return head + form # + '<p>' + CSV # + '<p>' + str(descr)
-    else:
-        # form submission
-        # build list of (etudid, note) and check it
-        notes = [ (etudid, tf.result['note_'+etudid]) for etudid in etudids ]
-        L, invalids, withoutnotes, absents, tosuppress = _check_notes(notes, E)
-        oknow = int(not len(invalids))
-        existing_decisions = []
-        if oknow:
-            nbchanged, nbsuppress, existing_decisions = _notes_add(context, authuser, evaluation_id, L, do_it=False )
-            msg_chg = ' (%d modifiées, %d supprimées)' % (nbchanged, nbsuppress)
-        else:
-            msg_chg = ''
-        # Affiche infos et messages d'erreur
-        H = ['<ul class="tf-msg">']
-        if invalids:
-            H.append( '<li class="tf-msg">%d notes invalides !</li>' % len(invalids) )
-        if len(L):
-             H.append( '<li class="tf-msg-notice">%d notes valides%s</li>' % (len(L), msg_chg) )
-        if withoutnotes:
-            H.append( '<li class="tf-msg-notice">%d étudiants sans notes !</li>' % len(withoutnotes) )
-        if absents:
-            H.append( '<li class="tf-msg-notice">%d étudiants absents !</li>' % len(absents) )
-        if tosuppress:
-            H.append( '<li class="tf-msg-notice">%d notes à supprimer !</li>' % len(tosuppress) )
-        if existing_decisions:
-            H.append( """<li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour %d étudiants. Après changement des notes, vérifiez la situation !</li>""" % len(existing_decisions))
-        H.append( '</ul>' )
-        H.append("""<p class="redboldtext">Les notes ne sont pas enregistrées; n'oubliez pas d'appuyer sur le bouton en bas du formulaire.</p>""")
-        
-        tf.formdescription.append(
-            ('okbefore', { 'input_type':'hidden', 'default' : oknow } ) )
-        tf.values['okbefore'] = oknow        
-        #tf.formdescription.append(
-        # ('reviewed', { 'input_type':'hidden', 'default' : oknow } ) )        
-        if oknow and okbefore and not changed:
-            # ---------------  ok, on rentre ces notes
-            nbchanged, nbsuppress, existing_decisions = _notes_add(context, authuser, evaluation_id, L, tf.result['comment'])
-            if nbchanged > 0 or nbsuppress > 0:
-                Mod['moduleimpl_id'] = M['moduleimpl_id']
-                Mod['url'] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % Mod
-                sco_news.add(context, REQUEST, typ=NEWS_NOTE, object=M['moduleimpl_id'],
-                             text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
-                             url=Mod['url'])
-            # affiche etat evaluation
-            etat = sco_evaluations.do_evaluation_etat(context, evaluation_id)         
-            msg = '%d notes / %d inscrits' % (
-                etat['nb_notes'], etat['nb_inscrits'])
-            if etat['nb_att']:
-                msg += ' (%d notes en attente)' % etat['nb_att']
-            if etat['evalcomplete'] or etat['evalattente']:
-                msg += """</p><p class="greenboldtext">Cette évaluation est prise en compte sur les bulletins et dans les calculs de moyennes"""
-                if etat['nb_att']:
-                    msg += ' (mais il y a des notes en attente !).'
-                else:
-                    msg += '.'
-            else:
-                msg += """</p><p class="fontred">Cette évaluation n'est pas encore prise en compte sur les bulletins et dans les calculs de moyennes car il manque des notes."""
-            if existing_decisions:
-                existing_msg = """<div class="ue_warning"><span>Important:</span> il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification de notes.</div>"""
-            else:
-                existing_msg = ''
-            #
-            return """<h3>%s</h3>
-            <p>%s notes modifiées (%d supprimées)<br/></p>
-            <p>%s</p>
-            %s
-            <p>
-            <a class="stdlink" href="moduleimpl_status?moduleimpl_id=%s">Aller au tableau de bord module</a>
-              
-            <a class="stdlink" href="notes_eval_selectetuds?evaluation_id=%s">Charger d'autres notes dans cette évaluation</a>
-            </p>
-            """ % (description,nbchanged,nbsuppress,msg,existing_msg,E['moduleimpl_id'],evaluation_id)
-        else:
-            if oknow:
-                tf.submitlabel = 'Enregistrer ces notes'
-            else:        
-                tf.submitlabel = 'Vérifier ces notes'
-            return head + '\n'.join(H) + tf.getform()
-
-
-# ---------------------------------------------------------------------------------
-
-def _XXX_do_evaluation_upload_csv(context, REQUEST): # XXX UNUSED
-    """soumission d'un fichier CSV (evaluation_id, notefile)  [XXX UNUSED]
-    """
-    authuser = REQUEST.AUTHENTICATED_USER
-    evaluation_id = REQUEST.form['evaluation_id']
-    comment = REQUEST.form['comment']
-    E = context.do_evaluation_list( {'evaluation_id' : evaluation_id})[0]
-    # Check access
-    # (admin, respformation, and responsable_id)
-    if not context.can_edit_notes( authuser, E['moduleimpl_id'] ):
-        # XXX imaginer un redirect + msg erreur
-        raise AccessDenied('Modification des notes impossible pour %s'%authuser)
-    #
-    data = REQUEST.form['notefile'].read()
-    #log('data='+str(data))
-    data = data.replace('\r\n','\n').replace('\r','\n')
-    lines = data.split('\n')
-    # decode fichier
-    # 1- skip lines until !evaluation_id
-    n = len(lines)
-    i = 0
-    #log('lines='+str(lines))
-    while i < n:
-        if not lines[i]:
-            raise NoteProcessError('Format de fichier invalide ! (1)')
-        if lines[i].strip()[0] == '!':
-            break
-        i = i + 1
-    if i == n:
-        raise NoteProcessError('Format de fichier invalide ! (pas de ligne evaluation_id)')
-    eval_id = lines[i].split(CSV_FIELDSEP)[0].strip()[1:]
-    if eval_id != evaluation_id:
-        raise NoteProcessError("Fichier invalide: le code d\'évaluation de correspond pas ! ('%s' != '%s')"%(eval_id,evaluation_id))
-    # 2- get notes -> list (etudid, value)
-    notes = []
-    ni = i+1
-    try:
-        for line in lines[i+1:]:
-            line = line.strip()
-            if line:
-                fs = line.split(CSV_FIELDSEP)
-                etudid = fs[0].strip()
-                val = fs[5].strip()
-                if etudid:
-                    notes.append((etudid,val))
-            ni += 1
-    except:
-        raise NoteProcessError('Format de fichier invalide ! (erreur ligne %d)<br/>"%s"' % (ni, lines[ni]))
-    L, invalids, withoutnotes, absents, tosuppress = _check_notes(notes,E)
-    if len(invalids):
-        return '<p class="boldredmsg">Le fichier contient %d notes invalides</p>' % len(invalids)
-    else:
-        nb_changed, nb_suppress, existing_decisions = _notes_add(context, authuser, evaluation_id, L, comment )
-        return '<p>%d notes changées (%d sans notes, %d absents, %d note supprimées)</p>'%(nb_changed,len(withoutnotes),len(absents),nb_suppress) + '<p>' + str(notes)
-
-
 def do_evaluation_upload_xls(context, REQUEST):
     """
     Soumission d'un fichier XLS (evaluation_id, notefile)
@@ -589,7 +246,7 @@
         diag = 'Valeur %s invalide' % value
     if diag:
         return context.sco_header(REQUEST)\
-               + '<h2>%s</h2><p><a href="notes_eval_selectetuds?evaluation_id=%s">Recommencer</a>'\
+               + '<h2>%s</h2><p><a href="saisie_notes?evaluation_id=%s">Recommencer</a>'\
                % (diag, evaluation_id) \
                + context.sco_footer(REQUEST)
     # Confirm action
@@ -601,7 +258,7 @@
             n'a été rentrée seront affectés)</p>
             """ % (value, len(L)),
             dest_url="", REQUEST=REQUEST,
-            cancel_url="notes_eval_selectetuds?evaluation_id=%s" % evaluation_id,
+            cancel_url="saisie_notes?evaluation_id=%s" % evaluation_id,
             parameters={'evaluation_id' : evaluation_id, 'value' : value})
     # ok
     comment = 'Initialisation notes manquantes'
@@ -668,62 +325,6 @@
 
     return context.sco_header(REQUEST) + '\n'.join(H) + context.sco_footer(REQUEST)
 
-def convert_note_from_string(note, note_max, etudid=None, absents=[], tosuppress=[], invalids=[] ):
-    """converti une valeur (chaine saisie) vers une note numérique (float)
-    Les listes absents, tosuppress et invalids sont modifiées
-    """
-    invalid = False
-    note_value = None
-    note = note.replace(',','.')
-    if note[:3] == 'ABS':
-        note_value = None
-        absents.append(etudid)
-    elif note[:3] == 'NEU' or note[:3] == 'EXC':
-        note_value = NOTES_NEUTRALISE
-    elif  note[:3] == 'ATT':
-        note_value = NOTES_ATTENTE
-    elif note[:3] == 'SUP':
-        note_value = NOTES_SUPPRESS
-        tosuppress.append(etudid)
-    else:
-        try:
-            note_value = float(note)
-            if (note_value < NOTES_MIN) or (note_value > note_max):
-                raise ValueError
-        except:
-            invalids.append(etudid)
-            invalid = True
-
-    return note_value, invalid
-    
-def _check_notes( notes, evaluation ):
-    """notes is a list of tuples (etudid, value)
-    returns list of valid notes (etudid, float value)
-    and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
-    """
-    note_max = evaluation['note_max']
-    L = [] # liste (etudid, note) des notes ok (ou absent) 
-    invalids = [] # etudid avec notes invalides
-    withoutnotes = [] # etudid sans notes (champs vides)
-    absents = [] # etudid absents
-    tosuppress = [] # etudids avec ancienne note à supprimer
-    existingjury = [] # etudids avec decision de jury (sem et/ou UE) a revoir eventuellement
-    for (etudid, note) in notes:
-        note = str(note).strip().upper()
-        if note[:3] == 'DEM':
-            continue # skip !
-        if note:
-            value, invalid = convert_note_from_string(
-                note, note_max, 
-                etudid=etudid, absents=absents, tosuppress=tosuppress, invalids=invalids )
-            if not invalid:
-                L.append((etudid,value))
-        else:
-            withoutnotes.append(etudid)
-    
-    return L, invalids, withoutnotes, absents, tosuppress
-
-
 def _notes_add(context, uid, evaluation_id, notes, comment=None, do_it=True ):
     """
     Insert or update notes
@@ -814,42 +415,74 @@
     return nb_changed, nb_suppress, existing_decisions
 
 
-def notes_eval_selectetuds(context, evaluation_id, REQUEST=None):
-    """Dialogue saisie notes: choix methode et groupes
+def saisie_notes_tableur(context, evaluation_id, group_ids=[], REQUEST=None):
+    """Saisie des notes via un fichier Excel
     """
+    authuser = REQUEST.AUTHENTICATED_USER
     evals = context.do_evaluation_list( {'evaluation_id' : evaluation_id})
     if not evals:
         raise ScoValueError('invalid evaluation_id')
-    theeval = evals[0]
-
-    if theeval['description']:
-        page_title = 'Saisie "%s"' % theeval['description']
+    E = evals[0]
+    M = context.do_moduleimpl_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
+    formsemestre_id = M['formsemestre_id']
+    if not context.can_edit_notes( authuser, E['moduleimpl_id'] ):
+        return context.sco_header(REQUEST) + '<h2>Modification des notes impossible pour %s</h2>' % authusername\
+               + """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
+               avez l'autorisation d'effectuer cette opération)</p>
+               <p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
+               """ % E['moduleimpl_id'] + context.sco_footer(REQUEST)
+    
+    if E['description']:
+        page_title = 'Saisie des notes de "%s"' % E['description']
     else:
         page_title = 'Saisie des notes'
-    H = [ context.sco_header(REQUEST, page_title=page_title) ]
+
+    # Informations sur les groupes à afficher:
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids=group_ids, formsemestre_id=formsemestre_id,
+        select_all_when_unspecified=True,
+        etat=None, REQUEST=REQUEST)
     
-    formid = 'notesfile'
-    if not REQUEST.form.get('%s-submitted'%formid,False):
-        # not submitted, choix groupe
-        r = do_evaluation_selectetuds(context, REQUEST)
-        if r:
-            H.append(r)            
+    H = [ 
+        context.sco_header(REQUEST, page_title=page_title,
+                           javascripts=sco_groups_view.JAVASCRIPTS,
+                           cssstyles=sco_groups_view.CSSSTYLES,
+                           init_qtip = True),
+        sco_evaluations.evaluation_describe(context, evaluation_id=evaluation_id, REQUEST=REQUEST),
+        '''<span class="eval_title">Saisie des notes par fichier</span>'''
+        ]
 
+    # Menu choix groupe:
+    H.append("""<div id="group-tabs"><table><tr><td>""")
+    H.append( sco_groups_view.form_groups_choice(
+        context, groups_infos,
+        with_selectall_butt=False ))
+    H.append('</td></tr></table></div>')
+    
+    H.append( """<div class="saisienote_etape1">
+        <span class="titredivsaisienote">Etape 1 : </span>
+        <ul>
+        <li><a href="feuille_saisie_notes?evaluation_id=%s&%s" class="stdlink">obtenir le fichier tableur à remplir</a></li>
+        <li>ou <a class="stdlink" href="saisie_notes?evaluation_id=%s">aller au formulaire de saisie</a></li>
+        </ul>
+        </div>
+        """ % (evaluation_id,groups_infos.groups_query_args,evaluation_id))
+    
     H.append('''<div class="saisienote_etape2">
     <span class="titredivsaisienote">Etape 2 : chargement d'un fichier de notes</span>''' #'
              )
-
+    
     nf = TrivialFormulator( REQUEST.URL0, REQUEST.form, ( 
         ('evaluation_id', { 'default' : evaluation_id, 'input_type' : 'hidden' }),
         ('notefile',  { 'input_type' : 'file', 'title' : 'Fichier de note (.xls)', 'size' : 44 }),
         ('comment', { 'size' : 44, 'title' : 'Commentaire',
-                      'explanation':'(note: la colonne remarque du fichier excel est ignorée)' }),
+                      'explanation':'(la colonne remarque du fichier excel est ignorée)' }),
         ),
-                            formid=formid,
-                            submitlabel = 'Télécharger')
+        formid='notesfile',
+        submitlabel = 'Télécharger')
     if nf[0] == 0:
         H.append('''<p>Le fichier doit être un fichier tableur obtenu via
-        le formulaire ci-dessus, puis complété et enregistré au format Excel.
+        l'étape 1 ci-dessus, puis complété et enregistré au format Excel.
         </p>''')
         H.append(nf[1])
     elif nf[0] == -1:
@@ -862,21 +495,21 @@
             <a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">
             Revenir au tableau de bord du module</a>
                
-            <a class="stdlink" href="notes_eval_selectetuds?evaluation_id=%(evaluation_id)s">Charger d'autres notes dans cette évaluation</a>
-            </p>''' % theeval)
+            <a class="stdlink" href="saisie_notes?evaluation_id=%(evaluation_id)s">Charger d'autres notes dans cette évaluation</a>
+            </p>''' % E)
         else:
             H.append('''<p class="redboldtext">Notes non chargées !</p>'''            
                      + updiag[1] )
             H.append('''
-            <p><a class="stdlink" href="notes_eval_selectetuds?evaluation_id=%(evaluation_id)s">
+            <p><a class="stdlink" href="saisie_notes_tableur?evaluation_id=%(evaluation_id)s">
             Reprendre</a>
-            </p>''' % theeval)
+            </p>''' % E)
     #
     H.append('''</div><h3>Autres opérations</h3><ul>''')
-    if context.can_edit_notes(REQUEST.AUTHENTICATED_USER,theeval['moduleimpl_id'],allow_ens=False):
+    if context.can_edit_notes(REQUEST.AUTHENTICATED_USER,E['moduleimpl_id'],allow_ens=False):
         H.append('''
         <li>
-        <form action="do_evaluation_set_missing" method="GET">
+        <form action="do_evaluation_set_missing" method="get">
         Mettre toutes les notes manquantes à <input type="text" size="5" name="value"/>
         <input type="submit" value="OK"/> 
         <input type="hidden" name="evaluation_id" value="%s"/> 
@@ -885,20 +518,20 @@
         </li>        
         <li><a class="stdlink" href="evaluation_suppress_alln?evaluation_id=%s">Effacer toutes les notes de cette évaluation</a> (ceci permet ensuite de supprimer l'évaluation si besoin)
         </li>''' % (evaluation_id, evaluation_id)) #'
-    H.append('''<li><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">Revenir au module</a>
-    </li>
-    </ul>''' % theeval )
+    H.append('''<li><a class="stdlink" href="moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s">Revenir au module</a></li>
+    <li><a class="stdlink" href="saisie_notes?evaluation_id=%(evaluation_id)s">Revenir au formulaire de saisie</a></li>
+    </ul>''' % E )
     
     H.append("""<h3>Explications</h3>
 <ol>
-<li>Cadre bleu (étape 1): 
-<ol><li>choisir la méthode de saisie (formulaire web ou feuille Excel);
-    <li>choisir le ou les groupes;</li>
+<li>Etape 1: 
+<ol><li>choisir le ou les groupes d'étudiants;</li>
+    <li>télécharger le fichier Excel à remplir.</li>
 </ol>
 </li>
-<li>Cadre vert (étape 2): à n'utiliser que si l'on est passé par une feuille Excel. Indiquer le fichier Excel <em>téléchargé à l'étape 1</em> et dans lequel on a saisi des notes. Remarques:
+<li>Etape 2 (cadre vert): Indiquer le fichier Excel <em>téléchargé à l'étape 1</em> et dans lequel on a saisi des notes. Remarques:
 <ul>
-<li>le fichier Excel ne doit pas forcément être complet: on peut ne saisir que quelques notes et répéter l'opération (en téléchargeant un nouveau fichier) plus tard;</li>
+<li>le fichier Excel peut être incomplet: on peut ne saisir que quelques notes et répéter l'opération (en téléchargeant un nouveau fichier) plus tard;</li>
 <li>seules les valeurs des notes modifiées sont prises en compte;</li>
 <li>seules les notes sont extraites du fichier Excel;</li>
 <li>on peut optionnellement ajouter un commentaire (type "copies corrigées par Dupont", ou "Modif. suite à contestation") dans la case "Commentaire".
@@ -911,6 +544,60 @@
     H.append( context.sco_footer(REQUEST) )
     return '\n'.join(H)
 
+
+def feuille_saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None):
+    """Document Excel pour saisie notes dans l'évaluation et les groupes indiqués
+    """
+    evals = context.do_evaluation_list( {'evaluation_id' : evaluation_id})
+    if not evals:
+        raise ScoValueError('invalid evaluation_id')
+    E = evals[0]
+    M = context.do_moduleimpl_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
+    formsemestre_id = M['formsemestre_id']
+    Mod = context.do_module_list( args={ 'module_id' : M['module_id'] } )[0]
+    sem = sco_formsemestre.get_formsemestre(context, M['formsemestre_id'])
+    evalname = '%s-%s' % (Mod['code'],DateDMYtoISO(E['jour']))
+    if E['description']:
+        evaltitre = '%s du %s' % (E['description'],E['jour'])
+    else:
+        evaltitre = 'évaluation du %s' % E['jour']
+    description = '%s en %s (%s) resp. %s' % (evaltitre, Mod['abbrev'], Mod['code'], strcapitalize(M['responsable_id']))
+
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids=group_ids, formsemestre_id=formsemestre_id,
+        select_all_when_unspecified=True,
+        etat=None, REQUEST=REQUEST)
+    groups = sco_groups.listgroups(context, groups_infos.group_ids)
+    gr_title_filename = sco_groups.listgroups_filename(groups) 
+    gr_title = sco_groups.listgroups_abbrev(groups)
+    if None in [ g['group_name'] for g in groups ]: # tous les etudiants
+        getallstudents = True
+        gr_title = 'tous'
+        gr_title_filename = 'tous'
+    else:
+        getallstudents = False
+    etudids = sco_groups.do_evaluation_listeetuds_groups(
+        context, evaluation_id, groups, getallstudents=getallstudents, include_dems=True)
+    # Notes existantes
+    NotesDB = context._notes_getall(evaluation_id)
+    
+    # une liste de liste de chaines: lignes de la feuille de calcul
+    L = []
+    
+    etuds = _get_sorted_etuds(context, E, etudids, formsemestre_id)
+    for e in etuds:
+        etudid = e['etudid']
+        groups = sco_groups.get_etud_groups(context, etudid, sem)
+        grc = sco_groups.listgroups_abbrev(groups)
+        
+        L.append( [ '%s' % etudid, strupper(e['nom']), strcapitalize(strlower(e['prenom'])),
+                    e['inscr']['etat'],
+                    grc, e['val'], e['explanation'] ] )
+    
+    filename = 'notes_%s_%s.xls' % (evalname, gr_title_filename)
+    xls = sco_excel.Excel_feuille_saisie( E, sem['titreannee'], description, lines=L )
+    return sco_excel.sendExcelFile(REQUEST, xls, filename )
+
 def has_existing_decision(context, M, E, etudid):
     """Verifie s'il y a une validation pour cet etudiant dans ce semestre ou UE
     Si oui, return True
@@ -927,3 +614,309 @@
             return True # decision pour l'UE a laquelle appartient cette evaluation
     
     return False # pas de decision de jury affectee par cette note
+
+
+
+# -----------------------------
+# Nouveau formulaire saisie notes (2016)
+
+def saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None):
+    """Formulaire saisie notes d'une évaluation pour un groupe
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    authusername = str(authuser)
+    
+    evals = context.do_evaluation_list( {'evaluation_id' : evaluation_id})
+    if not evals:
+        raise ScoValueError('invalid evaluation_id')
+    E = evals[0]
+    M = context.do_moduleimpl_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
+    formsemestre_id = M['formsemestre_id']
+    # Check access
+    # (admin, respformation, and responsable_id)
+    if not context.can_edit_notes( authuser, E['moduleimpl_id'] ):
+        return context.sco_header(REQUEST) + '<h2>Modification des notes impossible pour %s</h2>' % authusername\
+               + """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
+               avez l'autorisation d'effectuer cette opération)</p>
+               <p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
+               """ % E['moduleimpl_id'] + context.sco_footer(REQUEST)
+    
+    # Informations sur les groupes à afficher:
+    groups_infos = sco_groups_view.DisplayedGroupsInfos(
+        context, group_ids=group_ids, formsemestre_id=formsemestre_id,
+        select_all_when_unspecified=True,
+        etat=None, REQUEST=REQUEST)
+
+    if E['description']:
+        page_title = 'Saisie "%s"' % E['description']
+    else:
+        page_title = 'Saisie des notes'
+
+    # HTML page:
+    H = [
+        context.sco_header(
+            REQUEST, page_title=page_title,
+            javascripts=sco_groups_view.JAVASCRIPTS + [ 'js/saisie_notes.js' ],
+            cssstyles=sco_groups_view.CSSSTYLES,
+            init_qtip = True),
+        sco_evaluations.evaluation_describe(context, evaluation_id=evaluation_id, REQUEST=REQUEST),
+        '<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>',
+        ]
+    H.append("""<div id="group-tabs"><table><tr><td>""")
+    H.append( sco_groups_view.form_groups_choice(
+        context, groups_infos,
+        with_selectall_butt=False ))
+    H.append('</td><td style="padding-left: 35px;">')
+    H.append( makeMenu( "Autres opérations", [
+        { 'title' : 'Saisie par fichier tableur',
+          'url' : '/saisie_notes_tableur?evaluation_id=%s&%s' % (E['evaluation_id'], groups_infos.groups_query_args)
+        },
+        { 'title' : 'Voir toutes les notes du module',
+          'url' : '/evaluation_listenotes?moduleimpl_id=%s' % E['moduleimpl_id'],
+        },
+        { 'title' : 'Effacer toutes les notes de cette évaluation',
+          'url' : '/evaluation_suppress_alln?evaluation_id=%s' % (E['evaluation_id'],)
+        }
+            
+        ],  base_url=context.absolute_url(), alone=True)
+        )
+    H.append("""</td></tr></table></div>""")
+    
+    # Le formulaire de saisie des notes:
+    H.append( _form_saisie_notes(context, E, M, groups_infos.group_ids, REQUEST=REQUEST) )
+    #
+    H.append('</div>') # /saisie_notes
+
+    H.append('''<div class="sco_help">
+    <p>Les modifications sont enregistrées au fur et à mesure.</p>
+    <h4>Codes spéciaux:</h4>
+    <ul>
+    <li>ABS: absent (compte comme un zéro)</li>
+    <li>EXC: excusé (note neutralisée)</li>
+    <li>SUPR: pour supprimer une note existante</li>
+    <li>ATT: note en attente (permet de publier une évaluation avec des notes manquantes)</li>
+    </ul>
+    </div>''')
+    
+    H.append( context.sco_footer(REQUEST) )
+    return '\n'.join(H)
+
+def _get_sorted_etuds(context, E, etudids, formsemestre_id):
+
+    NotesDB = context._notes_getall(E['evaluation_id']) # Notes existantes
+    cnx = context.GetDBConnexion()
+    etuds = []
+    for etudid in etudids:
+        # infos identite etudiant
+        e = scolars.etudident_list(cnx, { 'etudid' : etudid })[0]
+        scolars.format_etud_ident(e)
+        etuds.append(e) 
+        # infos inscription dans ce semestre
+        e['inscr'] = context.do_formsemestre_inscription_list(
+            {'etudid':etudid, 'formsemestre_id' : formsemestre_id})[0]
+        # Information sur absence (ne tient pas compte de la demi-journée)
+        jour_iso = DateDMYtoISO(E['jour'])
+        nbabs = context.Absences.CountAbs(etudid, jour_iso, jour_iso)
+        nbabsjust = context.Absences.CountAbsJust(etudid, jour_iso, jour_iso)
+        e['absinfo'] = ''
+        if nbabs:
+            if nbabsjust:
+                e['absinfo'] = 'absent justifié ce jour !  '
+            else:
+                e['absinfo'] = 'absent ce jour !  '
+        # Note actuelle de l'étudiant:
+        if NotesDB.has_key(etudid):
+            e['val'] = _displayNote(NotesDB[etudid]['value'])
+            comment = NotesDB[etudid]['comment']
+            if comment is None:
+                comment = ''
+            e['explanation'] = '%s (%s) %s' % (NotesDB[etudid]['date'].strftime('%d/%m/%y %Hh%M'),
+                                               NotesDB[etudid]['uid'], comment )
+        else:
+            e['val'] = ''
+            e['explanation'] = ''
+        # Démission ?
+        if e['inscr']['etat'] == 'D':
+            #if not e['val']:
+            e['val'] = 'DEM'
+            e['explanation']  = 'Démission'
+    
+    etuds.sort(key=lambda x: (x['nom'], x['prenom']))
+    
+    return etuds
+
+def _form_saisie_notes(context, E, M, group_ids, REQUEST=None):
+    """Formulaire HTML saisie des notes  dans l'évaluation E du moduleimpl M
+    pour les groupes indiqués.
+    """
+    evaluation_id = E['evaluation_id']
+    formsemestre_id = M['formsemestre_id']
+    groups = sco_groups.listgroups(context, group_ids)
+    if None in [ g['group_name'] for g in groups ]: # tous les etudiants
+        getallstudents = True
+        gr_title = 'tous'
+        gr_title_filename = 'tous'
+    else:
+        getallstudents = False
+        gr_title = sco_groups.listgroups_abbrev(groups)
+        gr_title_filename = sco_groups.listgroups_filename(groups) 
+
+    etudids = sco_groups.do_evaluation_listeetuds_groups(
+        context, evaluation_id, groups, getallstudents=getallstudents, include_dems=True)
+    if not etudids:
+        return '<div class="ue_warning"><span>Aucun étudiant sélectionné !</span></div>'
+    
+    # Decisions de jury existantes ?
+    decisions_jury = { etudid : has_existing_decision(context, M, E, etudid)
+                       for etudid in etudids }
+    nb_decisions = sum(decisions_jury.values()) # Nb de decisions de jury pour ce groupe
+
+    etuds = _get_sorted_etuds(context, E, etudids, formsemestre_id)
+    
+    # Build form:
+    descr = [
+        ('evaluation_id', { 'default' : evaluation_id, 'input_type' : 'hidden' }),
+        ('formsemestre_id',  { 'default' : formsemestre_id, 'input_type' : 'hidden' }),
+        ('group_ids', { 'default' : group_ids,  'input_type' : 'hidden', 'type':'list' }),
+        # ('note_method', { 'default' : note_method, 'input_type' : 'hidden'}),
+        ('comment', { 'size' : 44, 'title' : 'Commentaire',
+                      'return_focus_next' : True, }),
+        ('changed', {'default':"0", 'input_type' : 'hidden'}), # changed in JS
+        ('s2' , {'input_type' : 'separator', 'title': '<br/>'}),
+        ]
+    initvalues = {}
+    for e in etuds:
+        etudid = e['etudid']
+        disabled = (e['val'] == 'DEM')
+        if disabled:
+            classdem = ' etud_dem'
+            disabled_attr = 'disabled="%d"' % disabled
+        else:
+            classdem = ''
+            disabled_attr = ''
+
+        label = '<span class="%s">' % classdem + e['nomprenom'] + '</span>'
+          
+        # Historique des saisies de notes:
+        if not disabled:
+            explanation = '<span id="hist_%s">' % etudid + get_note_history_menu(context, evaluation_id, etudid) + '</span>'
+        
+        explanation = e['absinfo'] + explanation 
+        
+        # Lien modif decision de jury:
+        explanation += '<span id="jurylink_%s" class="jurylink"></span>' % etudid
+        
+        # Valeur actuelle du champ:
+        initvalues['note_'+etudid] = e['val']
+        label_link = '<a class="etudinfo" id="%s">%s</a>' % (etudid, label)
+        
+        # Element de formulaire:
+        descr.append( ('note_'+etudid, 
+                       { 'size' : 5, 'title' : label_link,
+                         'explanation':explanation,
+                         'return_focus_next' : True,
+                         'attributes' : ['class="note%s"' % classdem, 
+                                         disabled_attr,
+                                         'data-last-saved-value=%s' % e['val'],
+                                         'data-orig-value=%s' % e['val'],
+                                         'data-etudid=%s' % etudid,
+                                         ],
+                         } ) )
+    #
+    H = []
+    if nb_decisions > 0:
+        H.append('''<div class="saisie_warn">
+        <ul class="tf-msg">
+        <li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour %d étudiants. Après changement des notes, vérifiez la situation !</li>
+        </ul>
+        </div>''' % nb_decisions)
+    H.append('''<div id="saisie_msg" class="head_message"></div>''')
+    
+    tf =  TF( REQUEST.URL0, REQUEST.form, descr, initvalues=initvalues,
+              submitlabel='Terminer',
+              formid='formnotes'
+              )        
+    H.append( tf.getform() )  # check and init
+    if tf.canceled():
+        return REQUEST.RESPONSE.redirect( '%s/Notes/moduleimpl_status?moduleimpl_id=%s'
+                                          % (context.ScoURL(), M['moduleimpl_id']) )
+    elif (not tf.submitted()) or not tf.result:
+        # affiche formulaire
+        return '\n'.join(H)
+    else:
+        # form submission
+        # rien à faire
+        return REQUEST.RESPONSE.redirect( '%s/Notes/moduleimpl_status?moduleimpl_id=%s'
+                                          % (context.ScoURL(), M['moduleimpl_id']) )
+
+def save_note(context, etudid=None, evaluation_id=None, value=None, comment='', REQUEST=None):
+    """Enregistre une note (ajax)    
+    """
+    authuser = REQUEST.AUTHENTICATED_USER
+    log('save_note: evaluation_id=%s etudid=%s uid=%s value=%s'
+        % (evaluation_id, etudid, authuser, value))
+    E = context.do_evaluation_list( {'evaluation_id' : evaluation_id})[0]
+    M = context.do_moduleimpl_list(args={ 'moduleimpl_id' : E['moduleimpl_id']})[0]
+    Mod = context.do_module_list( args={ 'module_id' : M['module_id'] } )[0]
+    Mod['url']="Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % M
+    
+    result = { 'nbchanged' : 0 } # JSON
+    # Check access: admin, respformation, or responsable_id
+    if not context.can_edit_notes( authuser, E['moduleimpl_id'] ):
+        result['status'] = 'unauthorized'
+    else:
+        L, invalids, withoutnotes, absents, tosuppress = _check_notes( [(etudid, value)], E)
+        if L:
+            nbchanged, nbsuppress, existing_decisions = _notes_add(context, authuser, evaluation_id, L, comment=comment, do_it=True)
+            sco_news.add(context, REQUEST, 
+                         typ=NEWS_NOTE, object=M['moduleimpl_id'],
+                         text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
+                         url=Mod['url'],
+                         max_frequency=30*60 # 30 minutes
+                         )
+            result['nbchanged'] = nbchanged
+            result['existing_decisions'] = existing_decisions
+            if nbchanged > 0:
+                result['history_menu'] = get_note_history_menu(context, evaluation_id, etudid)
+            else:
+                result['history_menu'] = '' # no update needed
+        result['status'] = 'ok'
+    
+    #time.sleep(5)
+    return sendJSON(REQUEST, result)
+    
+def get_note_history_menu(context, evaluation_id, etudid):
+    """Menu HTML historique de la note"""
+    history = sco_undo_notes.get_note_history(context, evaluation_id, etudid)
+    if not history:
+        return ''
+    
+    H = []
+    if len(history) > 1: 
+        H.append('<select data-etudid="%s" class="note_history" onchange="change_history(this);">' % etudid)
+        envir = 'select'
+        item = 'option'
+    else:
+        # pas de menu
+        H.append('<span class="history">')
+        envir = 'span'
+        item = 'span'
+
+    first=True 
+    for i in history:        
+        jt = i['date'].strftime('le %d/%m/%Y à %H:%M') + ' (%s)' % i['user_name']
+        dispnote= _displayNote(i['value'])
+        if first:
+            nv = '' # ne repete pas la valeur de la note courante
+        else:
+            # ancienne valeur
+            nv='<span class="histvalue">: %s</span>' % dispnote
+        first=False
+        if i['comment']:
+            comment = ' <span class="histcomment">%s</span>' % i['comment']
+        else:
+            comment = ''
+        H.append('<%s data-note="%s">%s %s%s</%s>' % (item, dispnote, jt, nv, comment, item)) 
+
+    H.append('</%s>' % envir )
+    return '\n'.join(H)

Modified: branches/ScoDoc7/sco_undo_notes.py
===================================================================
--- branches/ScoDoc7/sco_undo_notes.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_undo_notes.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -165,3 +165,48 @@
                     origin = 'Généré par %s le ' % VERSION.SCONAME + timedate_human_repr() + ''
                     )
     return tab.make_page(context, format=format, REQUEST=REQUEST)
+
+
+
+def get_note_history(context, evaluation_id, etudid, REQUEST=None, fmt=''):
+    """Historique d'une note
+    = liste chronologique d'opérations, la plus récente d'abord
+    [ { 'value', 'date', 'comment', 'uid' } ]
+    """
+    cnx = context.GetDBConnexion()
+    cursor = cnx.cursor(cursor_factory=ScoDocCursor)
+
+    # Valeur courante
+    cursor.execute('''
+    SELECT * FROM notes_notes
+    WHERE evaluation_id=%(evaluation_id)s AND etudid=%(etudid)s 
+    ''',
+    { 'evaluation_id' : evaluation_id, 'etudid' : etudid } )
+    history = cursor.dictfetchall()
+    
+    # Historique
+    cursor.execute('''
+    SELECT * FROM notes_notes_log
+    WHERE evaluation_id=%(evaluation_id)s AND etudid=%(etudid)s 
+    ORDER BY date DESC''',
+    { 'evaluation_id' : evaluation_id, 'etudid' : etudid } )
+    
+    history += cursor.dictfetchall()
+
+    # Replace None comments by ''
+    # et cherche nom complet de l'enseignant:
+    for x in history:
+        x['comment'] = x['comment'] or ''
+        x['user_name'] = context.Users.user_info(x['uid'])['nomcomplet']
+
+    if fmt == 'json':
+        return sendJSON(REQUEST, history)
+    else:
+        return history
+
+"""
+from debug import *
+from sco_undo_notes import *
+context = go_dept(app, 'RT').Notes
+get_note_history(context, 'EVAL29740', 'EID28403')
+"""

Modified: branches/ScoDoc7/sco_utils.py
===================================================================
--- branches/ScoDoc7/sco_utils.py	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/sco_utils.py	2016-07-23 15:41:14 UTC (rev 1535)
@@ -288,7 +288,7 @@
 
 def unescape_html(s):
     """un-escape html entities"""
-    s = str(s).strip().replace( '&', '&' )
+    s = s.strip().replace( '&', '&' )
     s = s.replace('<','<')
     s = s.replace('>','>')
     return s

Modified: branches/ScoDoc7/static/css/scodoc.css
===================================================================
--- branches/ScoDoc7/static/css/scodoc.css	2016-07-18 19:48:23 UTC (rev 1534)
+++ branches/ScoDoc7/static/css/scodoc.css	2016-07-23 15:41:14 UTC (rev 1535)
@@ -22,6 +22,11 @@
  font-family : "Helvetica Neue",Helvetica,Arial,sans-serif;
 }
 
+h3 {
+ font-size: 14pt;
+ font-weight: bold;
+}
+
 .scotext {
  font-family : TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia, serif;
 }
@@ -37,6 +42,10 @@
   font-size: 1.17em;
 }
 
+form#group_selector {
+    display: inline;
+}
+
 #group_selector button {
   padding-top: 3px;
   padding-bottom: 3px;
@@ -125,7 +134,7 @@
     padding-bottom: 100px;
 }
 
-P.footer {
+p.footer {
     font-size: 80%;
     color: rgb(60,60,60);
     margin-top: 10px;
@@ -630,6 +639,13 @@
   color: red;
 }
 
+div.sco_help {
+  margin-top: 12px;
+  font-style: italic; 
+  color: navy;
+  background-color: rgb(200,200,220);
+}
+
 p.indent {
   padding-left: 2em;
 }
@@ -721,10 +737,40 @@
     margin-top: -15px;
 }
 
+span.eval_title {
+    font-weight: bold; 
+    font-size: 14pt;
+}
+#saisie_notes span.eval_title {
+    /* border-bottom: 1px solid rgb(100,100,100); */
+}
+
+#saisie_msg {
+    padding:  0px;
+}
+
+span.jurylink {
+    margin-left: 1.5em;
+}
+span.jurylink a {
+    color: red;
+    text-decoration: underline;
+}
+
 .eval_description p {
-    margin-bottom: 5px;
-    margin-top: 5px;
+    margin-left: 15px;
+    margin-bottom: 2px;
+    margin-top: 0px;
 }
+
+.eval_description span.resp {
+    font-weight: normal;
+}
+.eval_description span.resp a {
+    font-weight: normal;
+}
+
+
 span.eval_info {
     font-style: italic;
 }
@@ -812,6 +858,10 @@
     color: black;
 }
 
+#formnotes .tf-explanation {
+    font-size: 80%;
+}
+
 /*
 .formsemestre_menubar {
     border-top: 3px solid #67A7E3;
@@ -1532,6 +1582,34 @@
   font-size: 115%;
 }
 
+
+.etud_dem {
+  color: rgb(130,130,130);
+}
+
+input.note_invalid {
+ color: red;
+ background-color: yellow;
+}
+
+input.note_valid_new {
+ color: blue;
+}
+
+input.note_saved {
+ color: green;
+}
+
+input.note {
+}
+
+span.history {
+  font-style: italic;
+}
+span.histcomment {
+  font-style: italic;
+}
+
 /* ----- Absences ------ */
 td.matin {
   background-color: rgb(203,242,255);

Added: branches/ScoDoc7/static/js/saisie_notes.js
===================================================================
--- branches/ScoDoc7/static/js/saisie_notes.js	                        (rev 0)
+++ branches/ScoDoc7/static/js/saisie_notes.js	2016-07-23 15:41:14 UTC (rev 1535)
@@ -0,0 +1,74 @@
+// Formulaire saisie des notes
+
+$().ready(function(){
+    
+    $("#formnotes .note").bind("blur", valid_note);
+});
+
+function is_valid_note(v) {
+    if (!v)
+        return true; 
+    if (! v.match("^[0-9.]*$")) {
+        return (v=="ABS")||(v=="EXC")||(v=="SUPR")||(v=="ATT")||(v=="DEM");
+    } else {
+        var x = parseFloat(v);
+        return (x >= 0) && (x <= 20);
+    }
+}
+
+function valid_note(e) {
+    var v = this.value.trim().toUpperCase().replace(",", ".");
+    if (is_valid_note(v)) {
+        if (v && (v != $(this).attr('data-last-saved-value'))) {
+            this.className = "note_valid_new";
+            var etudid = $(this).attr('data-etudid');
+            save_note(this, v, etudid);
+        }
+    } else {
+        /* Saisie invalide */
+        this.className = "note_invalid";
+    }
+}
+
+function save_note(elem, v, etudid) {
+    var evaluation_id = $("#formnotes_evaluation_id").attr("value");
+    var formsemestre_id = $("#formnotes_formsemestre_id").attr("value");
+    $('#saisie_msg').html("en cours...");
+    $.post( 'save_note',
+            { 
+                'etudid' : etudid,
+                'evaluation_id' : evaluation_id,
+                'value' : v,
+                'comment' : $("#formnotes_comment").attr("value")
+            },
+            function(result) {
+                $('#saisie_msg').html("enregistré");
+                elem.className = "note_saved";                
+                if (result['nbchanged'] > 0) {
+                    // il y avait une decision de jury ?
+                    if (result.existing_decisions[0] == etudid) {
+                        if (v != $(elem).attr('data-orig-value')) {
+                            $("#jurylink_"+etudid).html('<a href="formsemestre_validation_etud_form?formsemestre_id=' + formsemestre_id + '&etudid=' + etudid + '">mettre à jour décision de jury</a>');
+                        } else {
+                            $("#jurylink_"+etudid).html('');
+                        }
+                    }                    
+                    // mise a jour menu historique
+                    if (result['history_menu']) {
+                        $("#hist_"+etudid).html(result['history_menu']);
+                    }
+                }
+                $(elem).attr('data-last-saved-value', v)
+            }
+          );
+}
+
+function change_history(e) {
+    var opt = e.selectedOptions[0];
+    var val = $(opt).attr("data-note");
+    var etudid = $(e).attr('data-etudid');
+    // le input associé a ce menu:
+    var input_elem = e.parentElement.parentElement.parentElement.childNodes[0];
+    input_elem.value = val;
+    save_note(input_elem, val, etudid);
+}
\ No newline at end of file


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