[scodoc-devel] [SVN] Scolar : [1744] Introduction d'un malus sur les notes d'UE

eviennet at lipn.univ-paris13.fr eviennet at lipn.univ-paris13.fr
Jeu 28 Déc 22:23:08 CET 2017


Une pièce jointe HTML a été nettoyée...
URL: https://listes.univ-paris13.fr/pipermail/scodoc-devel/attachments/20171228/bb7c2d9b/attachment-0001.htm 
-------------- section suivante --------------
Modified: branches/ScoDoc7/ZNotes.py
===================================================================
--- branches/ScoDoc7/ZNotes.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/ZNotes.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -279,6 +279,8 @@
     edit_ue_set_code_apogee = sco_edit_ue.edit_ue_set_code_apogee
     security.declareProtected(ScoView, 'formation_table_recap')
     formation_table_recap = sco_edit_ue.formation_table_recap
+    security.declareProtected(ScoChangeFormation, 'formation_add_malus_modules')
+    formation_add_malus_modules = sco_edit_module.formation_add_malus_modules
     
     security.declareProtected(ScoChangeFormation, 'matiere_create')
     matiere_create = sco_edit_matiere.matiere_create
@@ -637,6 +639,7 @@
          'ue_id', 'matiere_id', 'formation_id',
          'semestre_id', 'numero', 
          'code_apogee',
+         'module_type'
          #'ects'
          ),
         sortkey='numero',
@@ -645,6 +648,7 @@
                              'heures_tp' :  float_null_is_zero,
                              'numero' : int_null_is_zero,
                              'coefficient' : float_null_is_zero,
+                             'module_type' : int_null_is_zero
                              #'ects' : float_null_is_null
                              },
         )

Modified: branches/ScoDoc7/config/postupgrade-db.py
===================================================================
--- branches/ScoDoc7/config/postupgrade-db.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/config/postupgrade-db.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -541,6 +541,7 @@
     if 'notes_notes_evaluation_id_idx' not in list_table_index(cnx, 'notes_notes'):
         log('creating index on notes_notes')
         cursor.execute("CREATE INDEX notes_notes_evaluation_id_idx ON notes_notes (evaluation_id)")
+    
     # boursier (ajout API nov 2017)
     check_field(cnx, 'identite', 'boursier',
                 ['alter table identite add column boursier text'
@@ -558,8 +559,7 @@
     """INSERT INTO itemsuivi (etudid, situation) 
        SELECT etudid, debouche FROM admissions WHERE debouche is not null;
     """
-        ] )
-    
+        ] )    
     check_table( cnx, 'itemsuivi_tags', [
     """CREATE TABLE itemsuivi_tags (
     tag_id text default notes_newid('TG') PRIMARY KEY,
@@ -573,8 +573,13 @@
     PRIMARY KEY (tag_id, itemsuivi_id)
     ) WITH OIDS;""",
         ] )
+    
+    # Types de modules (pour malus)
+    check_field(cnx, 'notes_modules', 'module_type',
+                ['alter table notes_modules add column module_type int',
+                ])
+    
     # Add here actions to performs after upgrades:
-    
     cnx.commit()
     cnx.close()
 

Modified: branches/ScoDoc7/misc/createtables.sql
===================================================================
--- branches/ScoDoc7/misc/createtables.sql	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/misc/createtables.sql	2017-12-28 21:23:07 UTC (rev 1744)
@@ -344,7 +344,8 @@
 	numero int, -- ordre de presentation
 	abbrev text, -- nom court
 	ects real, -- nombre de credits ECTS (NON UTILISES)
-	code_apogee text  -- id de l'element pedagogique Apogee correspondant
+	code_apogee text,  -- id de l'element pedagogique Apogee correspondant
+	module_type int, -- NULL ou 0:defaut, 1: malus (NOTES_MALUS)
 ) WITH OIDS;
 
 CREATE TABLE notes_tags (

Modified: branches/ScoDoc7/notes_table.py
===================================================================
--- branches/ScoDoc7/notes_table.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/notes_table.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -188,8 +188,7 @@
         uedict = {} # public member: { ue_id : ue }
         self.uedict = uedict
         for modimpl in self._modimpls:
-            mod = context.do_module_list(args={'module_id' : modimpl['module_id']} )[0]
-            modimpl['module'] = mod # add module dict to moduleimpl
+            mod = modimpl['module'] # has been added here by do_formsemestre_moyennes
             if not mod['ue_id'] in uedict:
                 ue = context.do_ue_list(args={'ue_id' : mod['ue_id']})[0]
                 uedict[ue['ue_id']] = ue
@@ -539,6 +538,8 @@
         notes_bonus_gen = [] # liste des notes de sport et culture
         coefs_bonus_gen = []
 
+        ue_malus = 0. # malus à appliquer à cette moyenne d'UE
+        
         notes = NoteVector()
         coefs = NoteVector()
         coefs_mask = NoteVector() # 0/1, 0 si coef a ete annulé
@@ -551,46 +552,58 @@
             # module ne faisant pas partie d'une UE capitalisee
             val = self._modmoys[modimpl['moduleimpl_id']].get(etudid, 'NI')
             # si 'NI' probablement etudiant non inscrit a ce module
-            coef = modimpl['module']['coefficient']
-            if modimpl['ue']['type'] != UE_SPORT:
-                notes.append(val, name=modimpl['module']['code'])
+            if modimpl['module']['module_type'] == MODULE_STANDARD:
+                coef = modimpl['module']['coefficient']
+                if modimpl['ue']['type'] != UE_SPORT:
+                    notes.append(val, name=modimpl['module']['code'])
+                    try:
+                        sum_notes += val * coef
+                        sum_coefs += coef
+                        nb_notes = nb_notes + 1
+                        coefs.append(coef)
+                        coefs_mask.append(1)                    
+                        matiere_id = modimpl['module']['matiere_id']
+                        if matiere_id_last and matiere_id != matiere_id_last and matiere_sum_coefs:
+                            self._matmoys[matiere_id_last][etudid] = matiere_sum_notes / matiere_sum_coefs
+                            matiere_sum_notes = matiere_sum_coefs = 0.
+                        matiere_sum_notes += val * coef
+                        matiere_sum_coefs += coef
+                        matiere_id_last = matiere_id                    
+                    except:
+                        nb_missing = nb_missing + 1
+                        coefs.append(0)
+                        coefs_mask.append(0)
+
+                else: # UE_SPORT:
+                    # la note du module de sport agit directement sur la moyenne gen.
+                    try:
+                        notes_bonus_gen.append(float(val))
+                        coefs_bonus_gen.append(coef)
+                    except:
+                        # log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef))
+                        pass
+            elif modimpl['module']['module_type'] == MODULE_MALUS:
                 try:
-                    sum_notes += val * coef
-                    sum_coefs += coef
-                    nb_notes = nb_notes + 1
-                    coefs.append(coef)
-                    coefs_mask.append(1)                    
-                    matiere_id = modimpl['module']['matiere_id']
-                    if matiere_id_last and matiere_id != matiere_id_last and matiere_sum_coefs:
-                        self._matmoys[matiere_id_last][etudid] = matiere_sum_notes / matiere_sum_coefs
-                        matiere_sum_notes = matiere_sum_coefs = 0.
-                    matiere_sum_notes += val * coef
-                    matiere_sum_coefs += coef
-                    matiere_id_last = matiere_id                    
+                    ue_malus += val
                 except:
-                    nb_missing = nb_missing + 1
-                    coefs.append(0)
-                    coefs_mask.append(0)
-            
-            else: # UE_SPORT:
-                # la note du module de sport agit directement sur la moyenne gen.
-                try:
-                    notes_bonus_gen.append(float(val))
-                    coefs_bonus_gen.append(coef)
-                except:
-                    # log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef))
-                    pass
+                    pass # si non inscrit ou manquant, ignore
+            else:
+                raise ValueError('invalid module type (%s)' % modimpl['module']['module_type'])
         
         if matiere_id_last and matiere_sum_coefs:
             self._matmoys[matiere_id_last][etudid] = matiere_sum_notes / matiere_sum_coefs
+        
         # Calcul moyenne:
         if sum_coefs > 0:
-            moy = sum_notes / sum_coefs
+            moy = sum_notes / sum_coefs            
+            if ue_malus:
+                moy -= ue_malus
+                moy = max( NOTES_MIN, min(moy, 20.) )
             moy_valid = True
         else:
             moy = 'NA'
             moy_valid = False
-
+        
         # (experimental) recalcule la moyenne en utilisant une formule utilisateur
         expr_diag = {}
         formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id, cnx)
@@ -604,9 +617,16 @@
                 expr_diag['ue_id'] = ue_id
                 self.expr_diagnostics.append(expr_diag)
         
-        return dict(moy=moy, nb_notes=nb_notes, nb_missing=nb_missing, sum_coefs=sum_coefs,
-                    notes_bonus_gen=notes_bonus_gen, coefs_bonus_gen=coefs_bonus_gen,
-                    expr_diag=expr_diag)
+        return dict(
+            moy=moy,
+            nb_notes=nb_notes,
+            nb_missing=nb_missing,
+            sum_coefs=sum_coefs,
+            notes_bonus_gen=notes_bonus_gen,
+            coefs_bonus_gen=coefs_bonus_gen,
+            expr_diag=expr_diag,
+            ue_malus=ue_malus
+            )
 
     def comp_etud_moy_gen(self, etudid, cnx):
         """Calcule moyenne gen. pour un etudiant

Modified: branches/ScoDoc7/sco_bulletins.py
===================================================================
--- branches/ScoDoc7/sco_bulletins.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/sco_bulletins.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -323,6 +323,7 @@
         mod_attente = False
         mod = modimpl.copy()
         mod_moy = nt.get_etud_mod_moy(modimpl['moduleimpl_id'], etudid)  # peut etre 'NI'
+        is_malus = (mod['module']['module_type'] == MODULE_MALUS)
         if bul_show_abs_modules:
             mod_abs = [context.Absences.CountAbs(etudid=etudid, debut=debut_sem, fin=fin_sem, moduleimpl_id=modimpl['moduleimpl_id']), context.Absences.CountAbsJust(etudid=etudid, debut=debut_sem, fin=fin_sem, moduleimpl_id=modimpl['moduleimpl_id'])]
             mod['mod_abs_txt'] = fmt_abs(mod_abs)
@@ -332,8 +333,10 @@
         mod['mod_moy_txt'] = fmt_note(mod_moy)
         if mod['mod_moy_txt'][:2] == 'NA':
             mod['mod_moy_txt'] = '-'
-        mod['mod_coef_txt']= fmt_coef(modimpl['module']['coefficient'])
-        
+        if is_malus:
+            mod['mod_coef_txt'] = ''
+        else:
+            mod['mod_coef_txt'] = fmt_coef(modimpl['module']['coefficient'])
         if mod['mod_moy_txt'] != 'NI': # ne montre pas les modules 'non inscrit'
             mods.append(mod)
             mod['stats'] = nt.get_mod_stats(modimpl['moduleimpl_id'])
@@ -367,9 +370,11 @@
             mod['evaluations'] = []
             for e in evals:
                 e = e.copy()
-                mod['evaluations'].append(e)
                 if int(e['visibulletin']) == 1 or version == 'long':
-                    e['name'] = e['description'] or 'le %s' % e['jour']
+                    if is_malus:
+                        e['name'] = "Points de malus sur cette UE"
+                    else:
+                        e['name'] = e['description'] or 'le %s' % e['jour']
                 e['target_html'] = 'evaluation_listenotes?evaluation_id=%s&format=html&tf-submitted=1' % e['evaluation_id']
                 e['name_html'] = '<a class="bull_link" href="%s">%s</a>' % (e['target_html'], e['name'])
                 val = e['notes'].get(etudid, {'value':'NP'})['value'] # NA si etud demissionnaire
@@ -380,11 +385,17 @@
                 else:
                     e['note_txt'] = fmt_note(val, note_max=e['note_max'])
                     e['note_html'] = e['note_txt']
-                    e['coef_txt'] = fmt_coef(e['coefficient'])
+                    if is_malus:
+                        e['coef_txt'] = ''
+                    else:
+                        e['coef_txt'] = fmt_coef(e['coefficient'])
                 if e['evaluation_type'] == EVALUATION_RATTRAPAGE:
                     e['coef_txt'] = 'rat.'
                 if e['etat']['evalattente']:
                     mod_attente = True # une eval en attente dans ce module
+                if (not is_malus) or (val != 'NP'):
+                    mod['evaluations'].append(e) # ne liste pas les eval malus sans notes
+            
             # Evaluations incomplètes ou futures:
             mod['evaluations_incompletes'] = []
             if context.get_preference('bul_show_all_evals', formsemestre_id):
@@ -401,7 +412,7 @@
                         e['note_txt'] = e['note_html'] = ''
                         e['coef_txt'] = fmt_coef(e['coefficient'])
             # Classement
-            if bul_show_mod_rangs and mod['mod_moy_txt'] != '-':
+            if bul_show_mod_rangs and mod['mod_moy_txt'] != '-' and not is_malus:
                 rg = nt.mod_rangs[modimpl['moduleimpl_id']]
                 if mod_attente: # nt.get_moduleimpls_attente():
                     mod['mod_rang'] = '(attente)'

Modified: branches/ScoDoc7/sco_compute_moy.py
===================================================================
--- branches/ScoDoc7/sco_compute_moy.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/sco_compute_moy.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -163,6 +163,7 @@
     """
     diag_info = {} # message d'erreur formule
     moduleimpl_id = mod['moduleimpl_id']
+    is_malus = (mod['module']['module_type'] == MODULE_MALUS)
     sem = sco_formsemestre.get_formsemestre(context, mod['formsemestre_id'])
     etudids = context.do_moduleimpl_listeetuds(moduleimpl_id) # tous, y compris demissions
     # Inscrits au semestre (pour traiter les demissions):
@@ -197,7 +198,11 @@
                 diag_info.update({ 'msg' : 'plusieurs évaluations de rattrapage !',
                                    'moduleimpl_id' : moduleimpl_id })
             eval_rattr = e
-    
+
+    # Les modules MALUS ne sont jamais considérés en attente
+    if is_malus:
+        attente = False
+            
     # filtre les evals valides (toutes les notes entrées)        
     valid_evals = [ e for e in evals
                     if ((e['etat']['evalcomplete'] or e['etat']['evalattente']) and (e['note_max'] > 0)) ]
@@ -294,22 +299,24 @@
     inscr = context.do_formsemestre_inscription_list(
         args = { 'formsemestre_id' : formsemestre_id })
     etudids = [ x['etudid'] for x in inscr ]
-    mods = context.do_moduleimpl_list( args={ 'formsemestre_id' : formsemestre_id})
+    modimpls = context.do_moduleimpl_list( args={ 'formsemestre_id' : formsemestre_id})
     # recupere les moyennes des etudiants de tous les modules
     D = {}
     valid_evals = []
     valid_evals_per_mod = {} # { moduleimpl_id : eval }
     mods_att = []
     expr_diags = []
-    for mod in mods:
-        moduleimpl_id = mod['moduleimpl_id']
+    for modimpl in modimpls:
+        mod = context.do_module_list(args={'module_id' : modimpl['module_id']} )[0]
+        modimpl['module'] = mod # add module dict to moduleimpl (used by nt)
+        moduleimpl_id = modimpl['moduleimpl_id']
         assert not D.has_key(moduleimpl_id)
-        D[moduleimpl_id], valid_evals_mod, attente, expr_diag = do_moduleimpl_moyennes(context, nt, mod)
+        D[moduleimpl_id], valid_evals_mod, attente, expr_diag = do_moduleimpl_moyennes(context, nt, modimpl)
         valid_evals_per_mod[moduleimpl_id] = valid_evals_mod
         valid_evals += valid_evals_mod
         if attente:
-            mods_att.append(mod)
+            mods_att.append(modimpl)
         if expr_diag:
             expr_diags.append(expr_diag)
     #
-    return D, mods, valid_evals_per_mod, valid_evals, mods_att, expr_diags
+    return D, modimpls, valid_evals_per_mod, valid_evals, mods_att, expr_diags

Modified: branches/ScoDoc7/sco_edit_module.py
===================================================================
--- branches/ScoDoc7/sco_edit_module.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/sco_edit_module.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -34,6 +34,7 @@
 import sco_codes_parcours
 from TrivialFormulator import TrivialFormulator, TF
 import sco_formsemestre
+import sco_edit_ue
 import sco_tag_module
 
 _MODULE_HELP = """<p class="help">
@@ -187,6 +188,13 @@
         ('titre'    , { 'size' : 30, 'explanation' : 'nom du module' }),
         ('abbrev'    , { 'size' : 20, 'explanation' : 'nom abrégé (pour bulletins)' }),
         
+        ('module_type', { 'input_type' : 'menu', 'title' : 'Type',
+                          'explanation' : '',
+                          'labels' : ('Standard', 'Malus'),
+                          'allowed_values' : (str(MODULE_STANDARD), str(MODULE_MALUS)),
+                          'enabled' : unlocked,
+                        }),
+        
         ('heures_cours' , { 'size' : 4, 'type' : 'float', 'explanation' : 'nombre d\'heures de cours' }),
         ('heures_td'    , { 'size' : 4, 'type' : 'float', 'explanation' : 'nombre d\'heures de Travaux Dirigés' }),
         ('heures_tp'    , { 'size' : 4, 'type' : 'float', 'explanation' : 'nombre d\'heures de Travaux Pratiques' }),
@@ -229,7 +237,7 @@
         return REQUEST.RESPONSE.redirect(dest_url)
 
 
-# essai edition en ligne:
+# Edition en ligne du code Apogee
 def edit_module_set_code_apogee(context, id=None, value=None, REQUEST=None):
     "set UE code apogee"
     module_id = id
@@ -268,3 +276,59 @@
     H.append('</ul>')
     H.append(context.sco_footer(REQUEST))
     return '\n'.join(H)
+
+#   
+
+def formation_add_malus_modules(context, formation_id, titre=None, REQUEST=None):
+    """Création d'un module de "malus" dans chaque UE d'une formation
+    """
+    ue_list = context.do_ue_list( args={ 'formation_id' : formation_id } )
+    
+    for ue in ue_list:
+        # Un seul module de malus par UE:
+        nb_mod_malus = len([ mod for mod in context.do_module_list( args={ 'ue_id' : ue['ue_id'] } )
+                             if mod['module_type'] == MODULE_MALUS ])
+        if nb_mod_malus == 0:
+            ue_add_malus_module(context, ue['ue_id'], titre=titre, REQUEST=REQUEST)
+    
+    if REQUEST:
+        REQUEST.RESPONSE.redirect('ue_list?formation_id='+ formation_id )
+
+def ue_add_malus_module(context, ue_id, titre=None, code=None, REQUEST=None):
+    """Add a malus module in this ue
+    """
+    ue = context.do_ue_list( args={ 'ue_id' : ue_id } )[0]
+    
+    if titre is None:
+        titre = ''
+    if code is None:
+        code = 'MALUS%d' % ue['numero']
+    
+    # Tout module doit avoir un semestre_id (indice 1, 2, ...)
+    semestre_ids = sco_edit_ue.ue_list_semestre_ids(context, ue)
+    if semestre_ids:
+        semestre_id = semestre_ids[0]
+    else:
+        # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement
+        # le semestre ? ou affecter le malus au semestre 1 ???
+        raise ScoValueError("Impossible d'ajouter un malus s'il n'y a pas d'autres modules")
+
+    # Matiere pour placer le module malus
+    Matlist = context.do_matiere_list( args={ 'ue_id' : ue_id } )
+    numero = max( [ mat['numero'] for mat in Matlist ] ) + 10
+    matiere_id = context.do_matiere_create(
+        { 'ue_id' : ue_id, 'titre' : 'Malus', 'numero' : numero }, REQUEST )
+    
+    module_id = context.do_module_create( {
+        'titre' : titre, 
+        'code' : code, 
+        'coefficient' : 0., # unused
+        'ue_id' : ue_id,
+        'matiere_id' : matiere_id,
+        'formation_id' : ue['formation_id'],
+        'semestre_id' : semestre_id,
+        'module_type' : MODULE_MALUS,
+        }, REQUEST )      
+
+    return module_id
+

Modified: branches/ScoDoc7/sco_edit_ue.py
===================================================================
--- branches/ScoDoc7/sco_edit_ue.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/sco_edit_ue.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -144,7 +144,7 @@
                       'matiere_id' : matiere_id,
                       'formation_id' : formation_id,
                       'semestre_id' : tf[2]['semestre_id'], 
-                      }, REQUEST )                      
+                      }, REQUEST )                
         else:
             ue_id = do_ue_edit(context, tf[2])
         return REQUEST.RESPONSE.redirect( REQUEST.URL1 + '/ue_list?formation_id=' + formation_id )
@@ -270,10 +270,14 @@
     H.append('<div class="fd_d"><span class="fd_t">Type parcours:</span><span class="fd_v">%s</span></div>' % parcours.__doc__ )
     if parcours.UE_IS_MODULE:
         H.append('<div class="fd_d"><span class="fd_t"> </span><span class="fd_n">(Chaque module est une UE)</span></div>' )
-    H.append('<div><a href="formation_edit?formation_id=%(formation_id)s" class="stdlink">modifier ces informations</a></div>' % F )
+
+    if editable:
+        H.append('<div><a href="formation_edit?formation_id=%(formation_id)s" class="stdlink">modifier ces informations</a></div>' % F )
+    
     H.append('</div>')
     
     # Description des UE/matières/modules
+    H.append('<div class="formation_ue_list">')
     H.append('<div class="ue_list_tit">Programme pédagogique:</div>')
 
     H.append('<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>')
@@ -328,15 +332,24 @@
         Matlist = context.do_matiere_list( args={ 'ue_id' : UE['ue_id'] } )
         for Mat in Matlist:
             if not parcours.UE_IS_MODULE:                
-                H.append('<li class="notes_matiere_list">%(titre)s' % Mat)
+                H.append('<li class="notes_matiere_list">')
                 if editable and not context.matiere_is_locked(Mat['matiere_id']):
-                    H.append('<a class="stdlink" href="matiere_edit?matiere_id=%(matiere_id)s">modifier</a>' % Mat)
+                    H.append('<a class="stdlink" href="matiere_edit?matiere_id=%(matiere_id)s">' % Mat)
+                H.append('%(titre)s' % Mat)
+                if editable and not context.matiere_is_locked(Mat['matiere_id']):
+                    H.append('</a>')
+            
             H.append('<ul class="notes_module_list">')
             Modlist = context.do_module_list( args={ 'matiere_id' : Mat['matiere_id'] } )
             im = 0
             for Mod in Modlist:
                 Mod['nb_moduleimpls'] = context.module_count_moduleimpls(Mod['module_id'])
-                H.append('<li class="notes_module_list">')
+                klass = 'notes_module_list'
+                if Mod['module_type'] == MODULE_MALUS:
+                    klass += ' module_malus'
+                H.append('<li class="%s">' % klass)
+
+                H.append('<span class="notes_module_list_buts">')
                 if im != 0 and editable:
                     H.append('<a href="module_move?module_id=%s&amp;after=0" class="aud">%s</a>' % (Mod['module_id'], arrow_up))
                 else:
@@ -351,10 +364,12 @@
                              % (Mod['module_id'], delete_icon))
                 else:
                     H.append(delete_disabled_icon)
+                H.append('</span>')
+                
                 mod_editable = editable # and not context.module_is_locked(Mod['module_id'])
                 if mod_editable:
-                    H.append('<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">' % Mod)                    
-                H.append('%(code)s %(titre)s' % Mod )
+                    H.append('<a class="discretelink" title="Modifier le module numéro %(numero)s, utilisé par %(nb_moduleimpls)d sessions" href="module_edit?module_id=%(module_id)s">' % Mod)
+                H.append('<span class="formation_module_tit">%s</span>' % join_words( Mod['code'], Mod['titre'] ))
                 if mod_editable:
                     H.append('</a>')
                 heurescoef = '%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s' % Mod
@@ -389,10 +404,12 @@
             H.append('<li><a class="stdlink" href="matiere_create?ue_id=%(ue_id)s">créer une matière</a> </li>' % UE)
         if not parcours.UE_IS_MODULE:
             H.append('</ul>')
+    H.append('</ul>')
     if editable:
-        H.append('<a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>' % formation_id)
-    H.append('</ul>')
-
+        H.append('<ul><li><a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>' % formation_id)
+        H.append('<li><a href="formation_add_malus_modules?formation_id=%(formation_id)s" class="stdlink">Ajouter des modules de malus dans chaque UE</a></li></ul>' % F )
+    H.append('</div>') # formation_ue_list
+    
     H.append('<p><ul>')
     if editable:
         H.append("""
@@ -434,7 +451,7 @@
     H.append(warn)
     
     H.append(context.sco_footer(REQUEST))
-    return '\n'.join(H)
+    return ''.join(H)
 
 
 def ue_sharing_code(context, ue_code=None, ue_id=None, hide_ue_id=None):
@@ -583,3 +600,15 @@
         preferences=context.get_preferences()
         )
     return  tab.make_page(context, format=format, REQUEST=REQUEST)      
+
+
+def ue_list_semestre_ids(context, ue):
+    """Liste triée des numeros de semestres des modules dans cette UE
+    Il est recommandable que tous les modules d'une UE aient le même indice de semestre.
+    Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
+    aussi ScoDoc laisse le choix.
+    """
+    Modlist = context.do_module_list( args={ 'ue_id' : ue['ue_id'] } )
+    return sorted(list(set([ mod['semestre_id'] for mod in Modlist ])))
+
+

Modified: branches/ScoDoc7/sco_evaluations.py
===================================================================
--- branches/ScoDoc7/sco_evaluations.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/sco_evaluations.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -137,6 +137,8 @@
     # ---- Liste des groupes complets et incomplets
     E = context.do_evaluation_list( args={ '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]
+    is_malus = Mod['module_type'] == MODULE_MALUS # True si module de malus
     formsemestre_id = M['formsemestre_id']
     # Si partition_id is None, prend 'all' ou bien la premiere:
     if partition_id is None:
@@ -195,15 +197,16 @@
 
     gr_incomplets = [ x for x in GrNbMissing.keys() ]
     gr_incomplets.sort()
-    if TotalNbMissing > 0 and E['evaluation_type'] != EVALUATION_RATTRAPAGE:
+    if (TotalNbMissing > 0) and (E['evaluation_type'] != EVALUATION_RATTRAPAGE) and not is_malus:
         complete = False
     else:
         complete = True            
-    if TotalNbMissing > 0 and (TotalNbMissing == TotalNbAtt or E['publish_incomplete'] != '0'):
+    if TotalNbMissing > 0 and (TotalNbMissing == TotalNbAtt or E['publish_incomplete'] != '0') and not is_malus:
         evalattente = True
     else:
         evalattente = False
-    # calcul moyenne dans chaque groupe de TD
+    
+    # Calcul moyenne dans chaque groupe de TD
     gr_moyennes = [] # group : {moy,median, nb_notes}
     for group_id in GrNotes.keys():
         notes = GrNotes[group_id]
@@ -364,6 +367,8 @@
     return etat
 
 def do_evaluation_etat_in_mod(context, nt, moduleimpl_id):
+    """
+    """
     evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
     etat = _eval_etat(evals)
     etat['attente'] = moduleimpl_id in [
@@ -552,17 +557,31 @@
 
     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>, notes sur <span id="eval_note_max">%g</span> ' 
-              % (E['coefficient'], E['note_max']))
+
+    etit = E['description'] or ''
+    if etit:
+        etit = ' "'+etit+'"'
+    if Mod['module_type'] == MODULE_MALUS:
+        etit += ' <span class="eval_malus">(points de malus)</span>'
+    H = [ '<span class="eval_title">Evaluation%s</span><p><b>Module : %s</b></p>' % (etit, mod_descr) ]
+    if Mod['module_type'] == MODULE_MALUS:
+        # Indique l'UE
+        ue = context.do_ue_list( args={ 'ue_id' : Mod['ue_id'] } )[0]
+        H.append( '<p><b>UE : %(acronyme)s</b></p>' % ue )
+        # store min/max values used by JS client-side checks:
+        H.append('<span id="eval_note_min" class="sco-hidden">-20.</span><span id="eval_note_max" class="sco-hidden">20.</span>')
+    else:
+        # date et absences (pas pour evals de malus)
+        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>, notes sur <span id="eval_note_max">%g</span> ' 
+                              % (E['coefficient'], E['note_max']))
+                H.append('<span id="eval_note_min" class="sco-hidden">0.</span>')
     if can_edit:
         H.append('<a href="evaluation_edit?evaluation_id=%s">(modifier l\'évaluation)</a>' % evaluation_id)
     H.append('</p>')
@@ -585,7 +604,8 @@
             {'evaluation_id' : evaluation_id})[0]    
         moduleimpl_id = the_eval['moduleimpl_id']
     #
-    M = context.do_moduleimpl_list( args={ 'moduleimpl_id' : moduleimpl_id } )[0]
+    M = context.do_moduleimpl_withmodule_list( args={ 'moduleimpl_id' : moduleimpl_id } )[0]
+    is_malus = M['module']['module_type'] == MODULE_MALUS # True si module de malus
     formsemestre_id = M['formsemestre_id']
     min_note_max = NOTES_PRECISION # le plus petit bareme possible
     if not readonly:
@@ -604,7 +624,9 @@
         if moduleimpl_id is None:
             raise ValueError, 'missing moduleimpl_id parameter'
         initvalues = { 'note_max' : 20,
-                       'jour' : time.strftime('%d/%m/%Y', time.localtime()) }
+                       'jour' : time.strftime('%d/%m/%Y', time.localtime()),
+                       'publish_incomplete' : is_malus,
+                     }
         submitlabel = 'Créer cette évaluation'
         action = 'Création d\'une é'
         link=''
@@ -656,6 +678,11 @@
     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>
+    <p class="help">
+    Les évaluations des modules de type "malus" sont spéciales: le coefficient n'est pas utilisé. 
+    Les notes de malus sont toujours comprises entre -20 et 20. Les points sont soustraits à la moyenne
+    de l'UE à laquelle appartient le module malus (si la note est négative, la moyenne est donc augmentée).
+    </p>
     """
     mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (moduleimpl_id, Mod['code'], Mod['titre'], link)
     if not readonly:
@@ -673,7 +700,7 @@
     if REQUEST.form.get('tf-submitted',False) and not REQUEST.form.has_key('visibulletinlist'):
         REQUEST.form['visibulletinlist'] = []        
     #
-    tf = TrivialFormulator( REQUEST.URL0, REQUEST.form, (
+    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' }),
@@ -683,7 +710,15 @@
                              '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 }),
+    ]
+    if is_malus: # pas de coefficient
+        form.append( ('coefficient', { 'input_type' : 'hidden', 'default' : "1." }) )
+    else:
+        form.append(
+            ('coefficient', { 'size' : 10, 'type' : 'float',
+                              'explanation' : 'coef. dans le module (choisi librement par l\'enseignant)',
+                              'allow_null':False }) )
+    form += [
         ('note_max'    , { 'size' : 4, 'type' : 'float', 'title' : 'Notes de 0 à', 
                            'explanation' : 'barème (note max actuelle: %s)' % min_note_max_str, 
                            'allow_null':False, 
@@ -703,7 +738,8 @@
                               'allowed_values' : (EVALUATION_NORMALE, EVALUATION_RATTRAPAGE),
                               'type' : 'int',
                               'labels' : ('Normale', 'Rattrapage') }),
-        ), 
+    ]
+    tf = TrivialFormulator( REQUEST.URL0, REQUEST.form, form, 
                             cancelbutton = 'Annuler',
                             submitlabel = submitlabel,
                             initvalues = initvalues, readonly=readonly)

Modified: branches/ScoDoc7/sco_formsemestre_status.py
===================================================================
--- branches/ScoDoc7/sco_formsemestre_status.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/sco_formsemestre_status.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -743,17 +743,27 @@
         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'])['prenomnom']))
+
+        if Mod['module_type'] == MODULE_STANDARD:
+            H.append('<td class="evals">')
+            nb_evals = etat['nb_evals_completes']+etat['nb_evals_en_cours']+etat['nb_evals_vides']
+            if nb_evals != 0:
+                H.append('<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">%s prévues, %s ok</a>' 
+                             % (M['moduleimpl_id'], nb_evals, etat['nb_evals_completes']))
+                if etat['nb_evals_en_cours'] > 0:
+                    H.append(', <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il manque des notes">%s en cours</a></span>' % (M['moduleimpl_id'], etat['nb_evals_en_cours']))
+                    if etat['attente']:
+                        H.append(' <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il y a des notes en attente">[en attente]</a></span>'
+                                     % M['moduleimpl_id'])
+        elif Mod['module_type'] == MODULE_MALUS:            
+            nb_malus_notes = sum( [ e['etat']['nb_notes']
+                                    for e in nt.get_mod_evaluation_etat_list(M['moduleimpl_id'])] )
+            H.append("""<td class="malus">
+            <a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">malus (%d notes)</a>
+            """ % (M['moduleimpl_id'], nb_malus_notes))
+        else:
+            raise ValueError('Invalid module_type') # a bug
         
-        H.append('<td class="evals">')
-        nb_evals = etat['nb_evals_completes']+etat['nb_evals_en_cours']+etat['nb_evals_vides']
-        if nb_evals != 0:
-            H.append('<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">%s prévues, %s ok</a>' 
-                     % (M['moduleimpl_id'], nb_evals, etat['nb_evals_completes']))
-            if etat['nb_evals_en_cours'] > 0:
-                H.append(', <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il manque des notes">%s en cours</a></span>' % (M['moduleimpl_id'], etat['nb_evals_en_cours']))
-            if etat['attente']:
-                H.append(' <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il y a des notes en attente">[en attente]</a></span>'
-                         % M['moduleimpl_id'])
         H.append('</td></tr>')
     H.append('</table></p>')
     # --- LISTE DES ETUDIANTS 

Modified: branches/ScoDoc7/sco_saisie_notes.py
===================================================================
--- branches/ScoDoc7/sco_saisie_notes.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/sco_saisie_notes.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -49,7 +49,7 @@
 from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC
 
 
-def convert_note_from_string(note, note_max, etudid=None, absents=[], tosuppress=[], invalids=[] ):
+def convert_note_from_string(note, note_max, note_min=NOTES_MIN, 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
     """
@@ -69,7 +69,7 @@
     else:
         try:
             note_value = float(note)
-            if (note_value < NOTES_MIN) or (note_value > note_max):
+            if (note_value < note_min) or (note_value > note_max):
                 raise ValueError
         except:
             invalids.append(etudid)
@@ -95,12 +95,19 @@
         val = '%g' % val
     return val
 
-def _check_notes( notes, evaluation ):
+def _check_notes( notes, evaluation, mod ):
     """notes is a list of tuples (etudid, value)
+    mod is the module (used to ckeck type, for malus)
     returns list of valid notes (etudid, float value)
     and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
     """
     note_max = evaluation['note_max']
+    if mod['module_type'] == MODULE_STANDARD:
+        note_min = NOTES_MIN
+    elif mod['module_type'] == MODULE_MALUS:
+        note_min = -20.
+    else:
+        raise ValueError('Invalid module type') # bug
     L = [] # liste (etudid, note) des notes ok (ou absent) 
     invalids = [] # etudid avec notes invalides
     withoutnotes = [] # etudid sans notes (champs vides)
@@ -113,7 +120,8 @@
             continue # skip !
         if note:
             value, invalid = convert_note_from_string(
-                note, note_max, 
+                note, note_max,
+                note_min=note_min,
                 etudid=etudid, absents=absents, tosuppress=tosuppress, invalids=invalids )
             if not invalid:
                 L.append((etudid,value))
@@ -131,6 +139,7 @@
     evaluation_id = REQUEST.form['evaluation_id']
     comment = REQUEST.form['comment']
     E = context.do_evaluation_list( {'evaluation_id' : evaluation_id})[0]
+    M = context.do_moduleimpl_withmodule_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
     # Check access
     # (admin, respformation, and responsable_id)
     if not context.can_edit_notes( authuser, E['moduleimpl_id'] ):
@@ -183,7 +192,7 @@
             diag.append('Erreur: feuille invalide ! (erreur ligne %d)<br/>"%s"' % (ni, str(lines[ni])))
             raise InvalidNoteValue()
         # -- check values
-        L, invalids, withoutnotes, absents, tosuppress = _check_notes(notes,E)
+        L, invalids, withoutnotes, absents, tosuppress = _check_notes(notes,E,M['module'])
         if len(invalids):
             diag.append('Erreur: la feuille contient %d notes invalides</p>' % len(invalids))
             if len(invalids) < 25:
@@ -224,6 +233,7 @@
     authuser = REQUEST.AUTHENTICATED_USER
     evaluation_id = REQUEST.form['evaluation_id']
     E = context.do_evaluation_list( {'evaluation_id' : evaluation_id})[0]
+    M = context.do_moduleimpl_withmodule_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
     # Check access
     # (admin, respformation, and responsable_id)
     if not context.can_edit_notes( authuser, E['moduleimpl_id'] ):
@@ -240,7 +250,7 @@
         if not NotesDB.has_key(etudid): # pas de note
             notes.append( (etudid, value) )
     # Check value
-    L, invalids, withoutnotes, absents, tosuppress = _check_notes(notes,E)
+    L, invalids, withoutnotes, absents, tosuppress = _check_notes(notes, E, M['module'])
     diag = ''
     if len(invalids):
         diag = 'Valeur %s invalide' % value
@@ -637,7 +647,7 @@
     if not evals:
         raise ScoValueError('invalid evaluation_id')
     E = evals[0]
-    M = context.do_moduleimpl_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
+    M = context.do_moduleimpl_withmodule_list( args={ 'moduleimpl_id' : E['moduleimpl_id'] } )[0]
     formsemestre_id = M['formsemestre_id']
     # Check access
     # (admin, respformation, and responsable_id)
@@ -800,14 +810,29 @@
         # ('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
-        ('s3' , {'input_type' : 'text', # affiche le barème
-                 'title' : 'Notes ',
-                 'cssclass' : 'formnote_bareme',
-                 'readonly' : True,
-                 'default': '&nbsp;/ %g' % E['note_max'],
-                 }),
+        ('changed', {'default':"0", 'input_type' : 'hidden'}), # changed in JS        
         ]
+    if M['module']['module_type'] == MODULE_STANDARD:
+        descr.append(
+            ('s3' , {
+             'input_type' : 'text', # affiche le barème
+             'title' : 'Notes ',
+             'cssclass' : 'formnote_bareme',
+             'readonly' : True,
+             'default': '&nbsp;/ %g' % E['note_max'],
+            }) )
+    elif M['module']['module_type'] == MODULE_MALUS:
+        descr.append(
+            ('s3' , {
+             'input_type' : 'text', # affiche le barème
+             'title' : '',
+             'cssclass' : 'formnote_bareme',
+             'readonly' : True,
+             'default': "Points de malus (soustraits à la moyenne de l'UE, entre -20 et 20)",
+            }) )
+    else:
+        raise ValueError('invalid module type (%s)' % M['module']['module_type']) # bug
+        
     initvalues = {}
     for e in etuds:
         etudid = e['etudid']
@@ -908,7 +933,7 @@
     if not context.can_edit_notes( authuser, E['moduleimpl_id'] ):
         result['status'] = 'unauthorized'
     else:
-        L, invalids, withoutnotes, absents, tosuppress = _check_notes( [(etudid, value)], E)        
+        L, invalids, withoutnotes, absents, tosuppress = _check_notes([(etudid, value)], E, Mod)        
         if L:
             nbchanged, nbsuppress, existing_decisions = _notes_add(context, authuser, evaluation_id, L, comment=comment, do_it=True)
             sco_news.add(context, REQUEST, 

Modified: branches/ScoDoc7/sco_utils.py
===================================================================
--- branches/ScoDoc7/sco_utils.py	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/sco_utils.py	2017-12-28 21:23:07 UTC (rev 1744)
@@ -64,7 +64,7 @@
 
 # ----- CALCUL ET PRESENTATION DES NOTES
 NOTES_PRECISION=1e-4 # evite eventuelles erreurs d'arrondis
-NOTES_MIN = 0.       # valeur minimale admise pour une note
+NOTES_MIN = 0.       # valeur minimale admise pour une note (sauf malus, dans [-20, 20])
 NOTES_MAX = 1000.
 NOTES_NEUTRALISE=-1000. # notes non prises en comptes dans moyennes
 NOTES_SUPPRESS=-1001.   # note a supprimer
@@ -108,6 +108,13 @@
               }
 UE_SEM_DEFAULT = 1000000 # indice semestre des UE sans modules
 
+# Types de modules
+MODULE_STANDARD = 0
+MODULE_MALUS = 1
+
+MALUS_MAX = 20.
+MALUS_MIN = -20.
+
 APO_MISSING_CODE_STR = '----' # shown in HTML pages in place of missing code Apogée
 EDIT_NB_ETAPES = 6 # Nombre max de codes étapes / semestre presentés dans l'UI
 
@@ -155,6 +162,9 @@
     """
     return "%s / %s" % (val[0], val[1])
 
+def join_words(*words):
+    words = [ str(w).strip() for w in words if w is not None ]
+    return ' '.join( [ w for w in words if w ] )
 
 def get_mention(moy):
     """Texte "mention" en fonction de la moyenne générale"""

Modified: branches/ScoDoc7/static/css/scodoc.css
===================================================================
--- branches/ScoDoc7/static/css/scodoc.css	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/static/css/scodoc.css	2017-12-28 21:23:07 UTC (rev 1744)
@@ -31,6 +31,10 @@
  font-family : TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia, serif;
 }
 
+.sco-hidden {
+    display: None;
+}
+
 div.tab-content {
   margin-top: 10px;
   margin-left: 15px;
@@ -925,6 +929,10 @@
 .eval_description span.resp a {
     font-weight: normal;
 }
+.eval_description span.eval_malus {
+    font-weight: bold;
+    color: red;
+}
 
 
 span.eval_info {
@@ -1126,7 +1134,7 @@
     border-bottom: 1px solid rgb(80%,80%,80%);
     border-left: 0px;
 }
-table.formsemestre_status td.evals, table.formsemestre_status th.evals, table.formsemestre_status td.resp, table.formsemestre_status th.resp {
+table.formsemestre_status td.evals, table.formsemestre_status th.evals, table.formsemestre_status td.resp, table.formsemestre_status th.resp, table.formsemestre_status td.malus {
     padding-left: 1em;
 }
 
@@ -1142,6 +1150,11 @@
     width: 2em;
     padding-right: 1em;
 }
+
+table.formsemestre_status td.malus a {
+    color: red;
+}
+
 a.formsemestre_status_link {
     text-decoration:none;
     color: black;
@@ -1379,12 +1392,14 @@
 /* Presentation formation (ue_list) */
 div.formation_descr {
   background-color: rgb(250,250,240);
-  padding-left: 0.7em;
-  margin-right: 1em;
+  border: 1px solid rgb(128,128,128);
+  padding-left: 5px;
+  padding-bottom: 5px;
+  margin-right: 12px;
 }
 div.formation_descr span.fd_t {
   font-weight: bold;
-  margin-right: 1em;
+  margin-right: 5px;
 }
 div.formation_descr span.fd_n {
   font-weight: bold;
@@ -1393,6 +1408,23 @@
   margin-left: 6em;
 }
 
+div.formation_ue_list {
+  border: 1px solid black;
+  margin-top: 5px;
+  margin-right: 12px;
+  padding-left: 5px;
+}
+
+li.module_malus span.formation_module_tit {
+  color: red;
+  font-weight: bold;
+  text-decoration: underline;
+}
+
+span.notes_module_list_buts {
+  margin-right: 5px;
+}
+
 div.ue_list_tit {
   font-weight: bold;
   margin-top: 5px;
@@ -1405,6 +1437,10 @@
   margin-right: 1em;
 }
 
+li.notes_ue_list {
+  margin-top: 9px;
+}
+
 span.ue_code {
   font-family: Courier, monospace;
   font-weight: normal;

Modified: branches/ScoDoc7/static/js/saisie_notes.js
===================================================================
--- branches/ScoDoc7/static/js/saisie_notes.js	2017-12-23 15:38:42 UTC (rev 1743)
+++ branches/ScoDoc7/static/js/saisie_notes.js	2017-12-28 21:23:07 UTC (rev 1744)
@@ -8,14 +8,15 @@
 function is_valid_note(v) {
     if (!v)
         return true; 
-
+    
+    var note_min = parseFloat($("#eval_note_min").text());
     var note_max = parseFloat($("#eval_note_max").text());
-
-    if (! v.match("^[0-9.]*$")) {
+    
+    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 <= note_max);
+        return (x >= note_min) && (x <= note_max);
     }
 }
 
@@ -74,4 +75,5 @@
     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