[scodoc-devel] [SVN] Scolar : [1725] Ajoute suivi devenir etudiants

eviennet at lipn.univ-paris13.fr eviennet at lipn.univ-paris13.fr
Lun 11 Déc 11:44:54 CET 2017


Une pièce jointe HTML a été nettoyée...
URL: https://listes.univ-paris13.fr/pipermail/scodoc-devel/attachments/20171211/0608375c/attachment-0001.htm 
-------------- section suivante --------------
Modified: branches/ScoDoc7/ZScolar.py
===================================================================
--- branches/ScoDoc7/ZScolar.py	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/ZScolar.py	2017-12-11 10:44:54 UTC (rev 1725)
@@ -88,6 +88,7 @@
 import sco_parcours_dut
 import sco_report
 import sco_archives_etud
+import sco_debouche
 import sco_groups_edit
 import sco_up_to_date
 import sco_edt_cal
@@ -861,14 +862,38 @@
                 situation += ' le ' + str(date_dem)
         return situation
 
+    # Debouche / devenir etudiant
+    
+    # vrai si l'utilisateur peut modifier les informations de suivi sur la page etud"
+    def can_edit_suivi(self, REQUEST=None):
+        authuser = REQUEST.AUTHENTICATED_USER
+        return authuser.has_permission(ScoEtudChangeAdr,self)
+    
+    security.declareProtected(ScoEtudChangeAdr, 'itemsuivi_suppress')
+    itemsuivi_suppress = sco_debouche.itemsuivi_suppress
+    security.declareProtected(ScoEtudChangeAdr, 'itemsuivi_create')
+    itemsuivi_create = sco_debouche.itemsuivi_create
+    security.declareProtected(ScoEtudChangeAdr, 'itemsuivi_set_date')
+    itemsuivi_set_date = sco_debouche.itemsuivi_set_date
+    security.declareProtected(ScoEtudChangeAdr, 'itemsuivi_set_situation')
+    itemsuivi_set_situation = sco_debouche.itemsuivi_set_situation
+    security.declareProtected(ScoView, 'itemsuivi_list_etud')
+    itemsuivi_list_etud = sco_debouche.itemsuivi_list_etud
+    security.declareProtected(ScoView, 'itemsuivi_tag_list')
+    itemsuivi_tag_list = sco_debouche.itemsuivi_tag_list
+    security.declareProtected(ScoView, 'itemsuivi_tag_search')
+    itemsuivi_tag_search = sco_debouche.itemsuivi_tag_search
+    security.declareProtected(ScoEtudChangeAdr, 'itemsuivi_tag_set')
+    itemsuivi_tag_set = sco_debouche.itemsuivi_tag_set
+    
     security.declareProtected(ScoEtudAddAnnotations, 'doAddAnnotation')
-    def doAddAnnotation(self, etudid, comment, author, REQUEST):
+    def doAddAnnotation(self, etudid, comment, REQUEST):
         "ajoute annotation sur etudiant"
         authuser = REQUEST.AUTHENTICATED_USER
         cnx = self.GetDBConnexion()
         scolars.etud_annotations_create(
             cnx,
-            args={ 'etudid':etudid, 'author' : author,
+            args={ 'etudid':etudid, 
                    'comment' : comment,
                    'zope_authenticated_user' : str(authuser),
                    'zope_remote_addr' : REQUEST.REMOTE_ADDR } )

Modified: branches/ScoDoc7/config/postupgrade-db.py
===================================================================
--- branches/ScoDoc7/config/postupgrade-db.py	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/config/postupgrade-db.py	2017-12-11 10:44:54 UTC (rev 1725)
@@ -545,6 +545,34 @@
     check_field(cnx, 'identite', 'boursier',
                 ['alter table identite add column boursier text'
                  ])
+    # Suivi des anciens etudiants (debouche)
+    # cree table suivi et recopie ancien champs debouche de la table admission
+    check_table( cnx, 'itemsuivi', [
+    """CREATE TABLE itemsuivi (
+    itemsuivi_id text DEFAULT notes_newid('SUI'::text) PRIMARY KEY,
+    etudid text NOT NULL,
+    item_date date DEFAULT now(),
+    situation text
+    ) WITH OIDS;""",
+
+    """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,
+    title text UNIQUE NOT NULL
+    ) WITH OIDS;""",
+        ] )
+    check_table( cnx, 'itemsuivi_tags_assoc', [
+    """CREATE TABLE itemsuivi_tags_assoc (
+    tag_id text REFERENCES itemsuivi_tags(tag_id) ON DELETE CASCADE,
+    itemsuivi_id text REFERENCES itemsuivi(itemsuivi_id) ON DELETE CASCADE,
+    PRIMARY KEY (tag_id, itemsuivi_id)
+    ) WITH OIDS;""",
+        ] )
     # Add here actions to performs after upgrades:
     
     cnx.commit()

Modified: branches/ScoDoc7/misc/createtables.sql
===================================================================
--- branches/ScoDoc7/misc/createtables.sql	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/misc/createtables.sql	2017-12-11 10:44:54 UTC (rev 1725)
@@ -100,7 +100,7 @@
     villelycee text,
     codepostallycee text,
     codelycee text,
-    debouche text, -- situation APRES etre passe par chez nous (texte libre)
+    debouche text, -- OBSOLETE UNUSED situation APRES etre passe par chez nous (texte libre)
     type_admission text, -- 'APB', 'APC-PC', 'CEF', 'Direct', '?' (autre)
     boursier_prec integer default NULL, -- etait boursier dans le cycle precedent (lycee) ?
     classement integer default NULL, -- classement par le jury d'admission (1 à N), global (pas celui d'APB si il y a des groupes)
@@ -108,6 +108,26 @@
     apb_classement_gr integer default NULL -- classement (1..Ngr) par le jury dans le groupe APB
 ) WITH OIDS;
 
+
+CREATE TABLE itemsuivi (
+    itemsuivi_id text DEFAULT notes_newid('SUI'::text) PRIMARY KEY,
+    etudid text NOT NULL,
+    item_date date DEFAULT now(), -- date de l'observation
+    situation text  -- situation à cette date (champ libre)
+) WITH OIDS;
+
+CREATE TABLE itemsuivi_tags (
+    tag_id text DEFAULT notes_newid('TG') PRIMARY KEY,
+    title text UNIQUE NOT NULL
+) WITH OIDS;
+
+CREATE TABLE itemsuivi_tags_assoc (
+    tag_id text REFERENCES itemsuivi_tags(tag_id) ON DELETE CASCADE,
+    itemsuivi_id text REFERENCES itemsuivi(itemsuivi_id) ON DELETE CASCADE,
+    PRIMARY KEY (tag_id, itemsuivi_id)
+) WITH OIDS;
+
+
 CREATE TABLE absences (
     etudid text NOT NULL,
     jour date, -- jour de l'absence
@@ -162,9 +182,9 @@
     id integer DEFAULT nextval('serial'::text) NOT NULL,
     date timestamp without time zone DEFAULT now(),
     etudid character(32),
-    author text,
+    author text, -- now unused
     comment text,
-    zope_authenticated_user text,
+    zope_authenticated_user text, -- should be author
     zope_remote_addr text
 ) WITH OIDS;
 

Modified: branches/ScoDoc7/sco_debouche.py
===================================================================
--- branches/ScoDoc7/sco_debouche.py	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/sco_debouche.py	2017-12-11 10:44:54 UTC (rev 1725)
@@ -30,13 +30,16 @@
 """
 
 from odict import odict
+import safehtml
 
 from notesdb import *
 from sco_utils import *
 from notes_log import log
+from scolog import logdb
 from gen_tables import GenTable
 import sco_formsemestre
 import sco_groups
+import sco_tag_module
 
 def report_debouche_date(context, start_year=None, format='html', REQUEST=None):
     """Rapport (table) pour les débouchés des étudiants sortis à partir de la l'année indiquée.
@@ -144,3 +147,174 @@
             +  context.sco_footer(REQUEST))
 
 
+# ----------------------------------------------------------------------------
+#
+# Nouveau suivi des etudiants (nov 2017)
+#
+# ----------------------------------------------------------------------------
+
+# OBSOLETE (this field has been copied to itemsuivi)
+# def debouche_set(context, object, value, REQUEST=None):
+#     """Set debouche (field in admission table, may be deprecated ?)
+#     """
+#     if not context.can_edit_suivi(REQUEST):
+#         raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+#     adm_id = object
+#     debouche = value.strip('-_ \t')
+#     cnx = context.GetDBConnexion()
+#     adms = scolars.admission_list(cnx, {'etudid' : etudid})
+#     if not adms:
+#         raise ValueError('no admission info for %s !' % etudid)
+#     adm = adms[0]
+#     adm['debouche'] = debouche
+#     admission_edit(cnx, adm)
+
+
+_itemsuiviEditor = EditableTable(
+    'itemsuivi',
+    'itemsuivi_id',
+    ('itemsuivi_id', 'etudid', 'item_date', 'situation',
+    ),
+    sortkey = 'item_date desc',
+    convert_null_outputs_to_empty=True,
+    output_formators = {
+        'situation' : safehtml.HTML2SafeHTML,
+        'item_date' : DateISOtoDMY,        
+        },
+    input_formators  = {
+        'item_date' : DateDMYtoISO,
+        },
+    )
+
+_itemsuivi_create = _itemsuiviEditor.create
+_itemsuivi_delete = _itemsuiviEditor.delete
+_itemsuivi_list   = _itemsuiviEditor.list
+_itemsuivi_edit   = _itemsuiviEditor.edit
+
+class ItemSuiviTag(sco_tag_module.ScoTag):
+    """Les tags sur les items
+    """
+    tag_table = 'itemsuivi_tags' # table (tag_id, title)
+    assoc_table = 'itemsuivi_tags_assoc' # table (tag_id, object_id)
+    obj_colname = 'itemsuivi_id' # column name for object_id in assoc_table
+
+
+def itemsuivi_get(cnx, itemsuivi_id):
+    """get an item"""
+    items = _itemsuivi_list(cnx, {'itemsuivi_id' : itemsuivi_id})
+    if items:
+        return items[0]
+    raise ValueError('invalid itemsuivi_id')
+
+def itemsuivi_suppress(context, itemsuivi_id, REQUEST=None):
+    """Suppression d'un item
+    """
+    if not context.can_edit_suivi(REQUEST):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    cnx = context.GetDBConnexion()
+    item = itemsuivi_get(cnx, itemsuivi_id)
+    _itemsuivi_delete(cnx, itemsuivi_id)
+    logdb(REQUEST,cnx,method='itemsuivi_suppress', etudid=item['etudid'])
+
+    
+def itemsuivi_create(context, etudid, item_date=None, situation='', REQUEST=None, format=None):
+    """Creation d'un item"""
+    if not context.can_edit_suivi(REQUEST):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")    
+    cnx = context.GetDBConnexion()
+    itemsuivi_id = _itemsuivi_create(cnx, args={
+        'etudid':etudid, 'item_date':item_date, 'situation' : situation })
+    logdb(REQUEST,cnx,method='itemsuivi_create', etudid=etudid)
+    #log('created itemsuivi %s for %s' % (itemsuivi_id, etudid))
+    item = itemsuivi_get(cnx, itemsuivi_id)
+    if format == 'json':
+        return sendJSON(REQUEST, item )
+    return item
+
+def itemsuivi_set_date(context, itemsuivi_id, item_date, REQUEST=None):
+    """set item date
+    item_date is a string dd/mm/yyyy
+    """
+    if not context.can_edit_suivi(REQUEST):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    #log('itemsuivi_set_date %s : %s' % (itemsuivi_id, item_date))
+    cnx = context.GetDBConnexion()
+    item = itemsuivi_get(cnx, itemsuivi_id)
+    item['item_date'] = item_date
+    _itemsuivi_edit(cnx, item)
+
+def itemsuivi_set_situation(context, object, value, REQUEST=None):
+    """set situation"""
+    if not context.can_edit_suivi(REQUEST):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    itemsuivi_id = object    
+    situation = value.strip('-_ \t')
+    #log('itemsuivi_set_situation %s : %s' % (itemsuivi_id, situation))    
+    cnx = context.GetDBConnexion()
+    item = itemsuivi_get(cnx, itemsuivi_id)
+    item['situation'] = situation
+    _itemsuivi_edit(cnx, item)
+    return situation or IT_SITUATION_MISSING_STR
+
+def itemsuivi_list_etud(context, etudid, format=None, REQUEST=None):
+    """Liste des items pour cet étudiant, avec tags"""
+    cnx = context.GetDBConnexion()
+    items = _itemsuivi_list(cnx, {'etudid' : etudid})
+    for it in items:
+        it['tags'] = ','.join(itemsuivi_tag_list(context, it['itemsuivi_id']))
+    if format == 'json':
+        return sendJSON(REQUEST, items );
+    return items
+    
+def itemsuivi_tag_list(context, itemsuivi_id):
+    """les noms de tags associés à cet item"""
+    r = SimpleDictFetch(context, '''SELECT t.title
+          FROM itemsuivi_tags_assoc a, itemsuivi_tags t
+          WHERE a.tag_id = t.tag_id
+          AND a.itemsuivi_id = %(itemsuivi_id)s
+          ''', { 'itemsuivi_id' : itemsuivi_id } )
+    return [ x['title'] for x in r ]
+
+def itemsuivi_tag_search(context, term, REQUEST=None ):
+    """List all used tag names (for auto-completion)"""
+    # restrict charset to avoid injections    
+    if not ALPHANUM_EXP.match(term.decode(SCO_ENCODING)):
+        data = []
+    else:
+        r = SimpleDictFetch(
+            context, 
+            "SELECT title FROM itemsuivi_tags WHERE title LIKE %(term)s", 
+            {'term' : term+'%'})
+        data = [ x['title'] for x in r ]
+    
+    return sendJSON(REQUEST, data)
+
+def itemsuivi_tag_set(context, itemsuivi_id='', taglist=[], REQUEST=None):
+    """taglist may either be:
+    a string with tag names separated by commas ("un;deux")
+    or a list of strings (["un", "deux"])
+    """
+    if not context.can_edit_suivi(REQUEST):
+        raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+    if not taglist:
+        taglist = []
+    elif type(taglist) == StringType:
+        taglist = taglist.split(',')
+    taglist = [ t.strip() for t in taglist ]
+    #log('itemsuivi_tag_set: itemsuivi_id=%s taglist=%s' % (itemsuivi_id, taglist))
+    # Sanity check:
+    cnx = context.GetDBConnexion()
+    item = itemsuivi_get(cnx, itemsuivi_id)
+    
+    newtags = set(taglist)
+    oldtags = set(itemsuivi_tag_list(context, itemsuivi_id))
+    to_del = oldtags - newtags
+    to_add = newtags - oldtags
+    
+    # should be atomic, but it's not.
+    for tagname in to_add:
+        t = ItemSuiviTag(context, tagname, object_id=itemsuivi_id)
+    for tagname in to_del:
+        t = ItemSuiviTag(context, tagname)
+        t.remove_tag_from_object(itemsuivi_id)
+

Modified: branches/ScoDoc7/sco_formsemestre_validation.py
===================================================================
--- branches/ScoDoc7/sco_formsemestre_validation.py	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/sco_formsemestre_validation.py	2017-12-11 10:44:54 UTC (rev 1725)
@@ -533,7 +533,7 @@
             for ue in ues:
                 ue_status = nt.get_etud_ue_status(etudid, ue['ue_id'])
                 H.append('<td class="ue">%g <span class="ects_fond">%g</span></td>' % (ue_status['ects_pot'],ue_status['ects_pot_fond']))
-            H.append('</tr>')
+            H.append('<td></td></tr>')
 
     H.append('</table>')
     return '\n'.join(H)

Modified: branches/ScoDoc7/sco_page_etud.py
===================================================================
--- branches/ScoDoc7/sco_page_etud.py	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/sco_page_etud.py	2017-12-11 10:44:54 UTC (rev 1725)
@@ -204,25 +204,18 @@
     # Liste des annotations
     alist = []
     annos = scolars.etud_annotations_list(cnx, args={ 'etudid' : etudid })
-    i = 0
     for a in annos:
-        if i % 2: # XXX refaire avec du CSS
-            a['bgcolor']="#EDEDED"
-        else:
-            a['bgcolor'] = "#DEDEDE"
-        i += 1
         if not context.canSuppressAnnotation(a['id'], REQUEST):
             a['dellink'] = ''
         else:
-            a['dellink'] = '<td bgcolor="%s" class="annodel"><a href="doSuppressAnnotation?etudid=%s&amp;annotation_id=%s">%s</a></td>' % (a['bgcolor'], etudid, a['id'], icontag('delete_img', border="0", alt="suppress", title="Supprimer cette annotation"))
-        alist.append('<tr><td bgcolor="%(bgcolor)s">Le %(date)s par <b>%(author)s</b> (%(zope_authenticated_user)s) :<br/>%(comment)s</td>%(dellink)s</tr>' % a )
+            a['dellink'] = '<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&amp;annotation_id=%s">%s</a></td>' % (etudid, a['id'], icontag('delete_img', border="0", alt="suppress", title="Supprimer cette annotation"))
+        alist.append('<tr><td><span class="annodate">Le %(date)s par %(zope_authenticated_user)s : </span><span class="annoc">%(comment)s</span></td>%(dellink)s</tr>' % a )
     info['liste_annotations'] = '\n'.join(alist)
     # fiche admission
     has_adm_notes = info['math'] or info['physique'] or info['anglais'] or info['francais']
     has_bac_info = info['bac'] or info['specialite'] or info['annee_bac'] or info['rapporteur'] or info['commentaire'] or info['classement'] or info['type_admission']
     if has_bac_info or has_adm_notes:
         adm_tmpl = """<!-- Donnees admission -->
-<div class="ficheadmission">
 <div class="fichetitre">Informations admission</div>
 """
         if has_adm_notes:
@@ -251,18 +244,33 @@
             adm_tmpl += "</div>"
         if info['rap']:
             adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
-        adm_tmpl += """</div></div>"""
+        adm_tmpl += """</div>"""
     else:
         adm_tmpl = '' # pas de boite "info admission"
     info['adm_data'] = adm_tmpl % info
 
     # Fichiers archivés:
-    info['fichiers_archive_htm'] = '<div class="ficheadmission"><div class="fichetitre">Fichiers associés</div>' + sco_archives_etud.etud_list_archives_html(context, REQUEST, etudid) + '</div>'
+    info['fichiers_archive_htm'] = '<div class="fichetitre">Fichiers associés</div>' + sco_archives_etud.etud_list_archives_html(context, REQUEST, etudid)
     
     # Devenir de l'étudiant:
-    has_debouche =  info['debouche']
+    has_debouche =  True # info['debouche']
+    if context.can_edit_suivi(REQUEST):
+        suivi_readonly = '0'
+        link_add_suivi = """<li class="adddebouche">
+            <a id="adddebouchelink" class="stdlink" href="#">ajouter une ligne</a>
+            </li>"""
+    else:
+        suivi_readonly = '1'
+        link_add_suivi = ''
     if has_debouche:
-        info['debouche_html'] = """<div class="fichedebouche"><span class="debouche_tit">Devenir:</span><span>%s</span></div>""" % info['debouche']
+        info['debouche_html'] = """<div id="fichedebouche" data-readonly="%s" data-etudid="%s">
+        <span class="debouche_tit">Devenir:</span>
+        <div><form>
+        <ul class="listdebouches">
+        %s
+        </ul>
+        </form></div>
+        </div>""" % (suivi_readonly, info['etudid'], link_add_suivi)
     else:
         info['debouche_html'] = '' # pas de boite "devenir"
     #
@@ -318,24 +326,27 @@
 
 %(inscriptions_mkup)s
 
+<div class="ficheadmission">
 %(adm_data)s
 
 %(fichiers_archive_htm)s
+</div>
 
 %(debouche_html)s
 
 <div class="ficheannotations">
 %(tit_anno)s
-<table width="95%%">%(liste_annotations)s</table>
+<table id="etudannotations">%(liste_annotations)s</table>
 
 <form action="doAddAnnotation" method="GET" class="noprint">
 <input type="hidden" name="etudid" value="%(etudid)s">
 <b>Ajouter une annotation sur %(nomprenom)s: </b>
 <table><tr>
 <tr><td><textarea name="comment" rows="4" cols="50" value=""></textarea>
-<br/><font size=-1><i>Balises HTML autorisées: b, a, i, br, p. Ces annotations sont lisibles par tous les enseignants et le secrétariat.</i></font>
+<br/><font size=-1><i>Ces annotations sont lisibles par tous les enseignants et le secrétariat.</i></font>
 </td></tr>
-<tr><td>Auteur : <input type="text" name="author" width=12 value="%(authuser)s">&nbsp;
+<tr><td>
+ <input type="hidden" name="author" width=12 value="%(authuser)s">
 <input type="submit" value="Ajouter annotation"></td></tr>
 </table>
 </form>
@@ -348,7 +359,15 @@
     header = context.sco_header(
                 REQUEST,
                 page_title='Fiche étudiant %(prenom)s %(nom)s'%info,
-                javascripts=['js/recap_parcours.js'])
+                cssstyles=['libjs/jQuery-tagEditor/jquery.tag-editor.css'],
+                javascripts=[
+                    'libjs/jinplace-1.2.1.min.js',
+                    'js/ue_list.js',
+                    'libjs/jQuery-tagEditor/jquery.tag-editor.min.js',
+                    'libjs/jQuery-tagEditor/jquery.caret.min.js',
+                    'js/recap_parcours.js',
+                    'js/etud_debouche.js',
+                    ])
     return header + tmpl % info + context.sco_footer(REQUEST)
 
 

Modified: branches/ScoDoc7/sco_permissions.py
===================================================================
--- branches/ScoDoc7/sco_permissions.py	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/sco_permissions.py	2017-12-11 10:44:54 UTC (rev 1725)
@@ -16,7 +16,7 @@
 
 ScoAbsChange    = "Sco Change Absences"
 ScoAbsAddBillet = "Sco Add Abs Billet" # ajouter un billet d'absence via AddBilletAbsence
-ScoEtudChangeAdr   = "Sco Change Etud Address" # changer adresse/photo ou pour envoyer bulletins par mail
+ScoEtudChangeAdr   = "Sco Change Etud Address" # changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche
 ScoEtudChangeGroups = "Sco Change Etud Groups"
 ScoEtudInscrit  = "Sco Inscrire Etud" # aussi pour demissions, diplomes
 ScoEtudAddAnnotations = "Sco Etud Add Annotations" # aussi pour archives

Modified: branches/ScoDoc7/sco_tag_module.py
===================================================================
--- branches/ScoDoc7/sco_tag_module.py	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/sco_tag_module.py	2017-12-11 10:44:54 UTC (rev 1725)
@@ -51,15 +51,23 @@
 #   module_tag_list( module_id ) -> les noms de tags associés à ce module
 #   module_tag_set( module_id, taglist ) -> modifie les tags
 
-class ModuleTag:
-    def __init__(self, context, title, module_id=''):
+class ScoTag:
+    """Generic tags for ScoDoc
+    """
+    
+    # must be overloaded:
+    tag_table = None # table (tag_id, title)
+    assoc_table = None # table (tag_id, object_id)
+    obj_colname = None # column name for object_id in assoc_table
+    
+    def __init__(self, context, title, object_id=''):
         """Load tag, or create if does not exist
         """
         self.context = context
         self.title = title.strip()
         if not self.title:
             raise ScoValueError('invalid empty tag')
-        r = SimpleDictFetch(context, "SELECT * FROM notes_tags WHERE title = %(title)s",
+        r = SimpleDictFetch(context, 'SELECT * FROM ' + self.tag_table + ' WHERE title = %(title)s',
                             { 'title' : self.title } )
         if r:
             self.tag_id = r[0]['tag_id']
@@ -67,11 +75,11 @@
             # Create new tag:
             log('creating new tag: %s' % self.title)
             cnx = context.GetDBConnexion()
-            oid = DBInsertDict(cnx, 'notes_tags', { 'title' : self.title }, commit=True)
+            oid = DBInsertDict(cnx, self.tag_table, { 'title' : self.title }, commit=True)
             self.tag_id = SimpleDictFetch(
-                context, "SELECT tag_id FROM notes_tags WHERE oid=%(oid)s", {'oid':oid})[0]['tag_id']
-        if module_id:
-            self.tag_module(module_id)
+                context, 'SELECT tag_id FROM ' + self.tag_table + ' WHERE oid=%(oid)s', {'oid':oid})[0]['tag_id']
+        if object_id:
+            self.tag_object(object_id)
 
     def __repr__(self): # debug
         return '<tag "%s">' % self.title
@@ -83,34 +91,35 @@
         args = { 'tag_id' : self.tag_id }
         SimpleQuery(
             self.context,
-            '''DELETE FROM notes_tags t WHERE t.tag_id = %(tag_id)s''', args )
+            'DELETE FROM ' + self.tag_table + ' t WHERE t.tag_id = %(tag_id)s', args )
     
-    def tag_module(self, module_id):
-        """Associate tag to module"""
-        log('tagging module %s with %s' % (module_id, self.title))
-        args = { 'module_id' : module_id, 'tag_id' : self.tag_id }
+    def tag_object(self, object_id):
+        """Associate tag to given object"""
+        args = { self.obj_colname : object_id, 'tag_id' : self.tag_id }
         r = SimpleDictFetch(
             self.context, 
-            '''SELECT * FROM notes_modules_tags mt
-            WHERE mt.module_id = %(module_id)s AND mt.tag_id = %(tag_id)s
-            ''', args )
+            'SELECT * FROM ' + self.assoc_table +
+            ' a WHERE a.' + self.obj_colname +
+            ' = %(' + self.obj_colname + ')s AND a.tag_id = %(tag_id)s',
+            args )
         if not r:
-            log('tag module %s with %s' % (module_id, self.title))
+            log('tag %s with %s' % (object_id, self.title))
             cnx = self.context.GetDBConnexion()
-            DBInsertDict(cnx, 'notes_modules_tags', args, commit=True)
+            DBInsertDict(cnx, self.assoc_table, args, commit=True)
     
-    def remove_tag_from_module(self, module_id):
+    def remove_tag_from_object(self, object_id):
         """Remove tag from module.
         If no more modules tagged with this tag, delete it.
         Return True if Tag still exists.
         """
-        log('removing tag %s from %s' % (self.title, module_id))
-        args = { 'module_id' : module_id, 'tag_id' : self.tag_id }
+        log('removing tag %s from %s' % (self.title, object_id))
+        args = { 'object_id' : object_id, 'tag_id' : self.tag_id }
         SimpleQuery(
             self.context,
-            '''DELETE FROM notes_modules_tags mt
-            WHERE mt.module_id = %(module_id)s AND mt.tag_id = %(tag_id)s
-            ''', args )
+            'DELETE FROM  ' + self.assoc_table +
+            ' a WHERE a.' + self.obj_colname + 
+            ' = %(object_id)s AND a.tag_id = %(tag_id)s',
+            args )
         r = SimpleDictFetch(
             self.context, 
             '''SELECT * FROM notes_modules_tags mt WHERE tag_id = %(tag_id)s
@@ -121,6 +130,14 @@
                 self.context,
                 '''DELETE FROM notes_tags t WHERE t.tag_id = %(tag_id)s''', args )
 
+
+class ModuleTag(ScoTag):
+    """Tags sur les modules dans les programmes pédagogiques
+    """
+    tag_table = 'notes_tags' # table (tag_id, title)
+    assoc_table = 'notes_modules_tags' # table (tag_id, object_id)
+    obj_colname = 'module_id' # column name for object_id in assoc_table
+    
     def list_modules(self, formation_code=''):
         """Liste des modules des formations de code donné (formation_code) avec ce tag
         """
@@ -129,10 +146,10 @@
             # tous les modules de toutes les formations !            
             r = SimpleDictFetch(
                 self.context, 
-                '''SELECT module_id FROM notes_modules_tags WHERE tag_id = %(tag_id)s
-                ''', args)
+                'SELECT ' + self.obj_colname + ' FROM ' + self.assoc_table + ' WHERE tag_id = %(tag_id)s', args)
         else:
             args['formation_code'] = formation_code
+            
             r = SimpleDictFetch(
                 self.context, 
                 '''SELECT mt.module_id 
@@ -148,8 +165,7 @@
 
 def module_tag_search(context, term, REQUEST=None):
     """List all used tag names (for auto-completion)"""
-    # restrict charset to avoid injections
-    log('module_tag_search %s' % term)
+    # restrict charset to avoid injections    
     if not ALPHANUM_EXP.match(term.decode(SCO_ENCODING)):
         data = []
     else:
@@ -158,7 +174,7 @@
             "SELECT title FROM notes_tags WHERE title LIKE %(term)s", 
             {'term' : term+'%'})
         data = [ x['title'] for x in r ]
-    log(data)
+    
     return sendJSON(REQUEST, data)
 
 def module_tag_list(context, module_id=''):
@@ -194,10 +210,10 @@
 
     # should be atomic, but it's not.
     for tagname in to_add:
-        t = ModuleTag(context, tagname, module_id=module_id)
+        t = ModuleTag(context, tagname, object_id=module_id)
     for tagname in to_del:
         t = ModuleTag(context, tagname)
-        t.remove_tag_from_module(module_id)
+        t.remove_tag_from_object(module_id)
 
 
 def get_etud_tagged_modules(context, etudid, tagname):

Modified: branches/ScoDoc7/sco_utils.py
===================================================================
--- branches/ScoDoc7/sco_utils.py	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/sco_utils.py	2017-12-11 10:44:54 UTC (rev 1725)
@@ -111,6 +111,8 @@
 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
 
+IT_SITUATION_MISSING_STR = '____' # shown on ficheEtud (devenir) in place of empty situation
+
 # borne supérieure de chaque mention
 NOTES_MENTIONS_TH = (NOTES_TOLERANCE, 7., 10., 12., 14., 16., 18., 20.+NOTES_TOLERANCE)
 NOTES_MENTIONS_LABS=('Nul', 'Faible', 'Insuffisant', 'Passable', 'Assez bien', 'Bien', 'Très bien', 'Excellent')

Modified: branches/ScoDoc7/static/css/scodoc.css
===================================================================
--- branches/ScoDoc7/static/css/scodoc.css	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/static/css/scodoc.css	2017-12-11 10:44:54 UTC (rev 1725)
@@ -446,7 +446,8 @@
 }
 
 div.ficheadmission { 
-  background-color: rgb( 231, 234, 218 ); /* E7EADA */   
+  background-color: rgb( 231, 234, 218 ); /* E7EADA */
+  
   margin: 0.5em 0 0.5em 0;
   padding: 0.5em;
   -moz-border-radius: 8px;
@@ -486,9 +487,10 @@
 span.deletudarchive {
     margin-left: 0.5em;
 }
-div.fichedebouche { 
-  background-color: rgb( 0, 100, 0 ); 
-  color: gold;
+div#fichedebouche { 
+  background-color: rgb(183, 227, 254); /* bleu clair */
+  color: navy;
+  width: 910px;
   margin: 0.5em 0 0.5em 0;
   padding: 0.5em;
   -moz-border-radius: 8px;
@@ -496,11 +498,65 @@
   border-radius: 8px;
 }
 
+div#fichedebouche .ui-accordion-content {
+  background-color: rgb(183, 227, 254); /* bleu clair */
+  padding: 0px 10px 0px 0px;  
+}
+
 span.debouche_tit {
   font-weight: bold;
   padding-right: 1em;
 }
 
+li.itemsuivi {
+}
+
+span.itemsuivi_tag_edit {
+    border: 2px;
+}
+.listdebouches .itemsuivi_tag_edit .tag-editor {
+    background-color: rgb(183, 227, 254);
+    border: 0px;
+}
+.itemsuivi_tag_edit ul.tag-editor {
+    display: inline-block;
+    width: 100%;
+}
+.itemsuivi_tag_edit ul.tag-editor li {
+/*    height: 15px; */
+}
+.itemsuivi_tag_edit .tag-editor-delete {
+    height: 20px;
+}
+div.itemsituation {
+    background-color: rgb(224, 234, 241);
+    /* height: 2em;*/
+    border: 1px solid rgb(204,204,204);
+    -moz-border-radius: 4px;
+   -khtml-border-radius: 4px;
+   border-radius: 4px;
+   padding-top: 1px;
+   padding-bottom: 1px;
+   padding-left: 10px;
+   padding-right: 10px;
+}
+div.itemsituation em {
+    color: #bbb;
+}
+
+/* tags readonly */
+span.ro_tag {
+    display: inline-block;
+    background-color: rgb(224, 234, 241);
+    color: #46799b;
+    margin-top: 3px;
+    margin-left: 5px;
+    margin-right: 3px;
+    padding-left: 3px;
+    padding-right: 3px;
+    border: 1px solid rgb(204,204,204);
+}
+
 div.ficheinscriptions {
    background-color: #eae3e2; /* was EADDDA */
    margin: 0.5em 0 0.5em 0;
@@ -562,7 +618,9 @@
 }
 
 .ficheannotations {
-  background-color: #ddffdd; 
+  background-color: #f7d892;
+  width: 910px;
+  
   margin: 0.5em 0 0.5em 0;
   padding: 0.5em;
   -moz-border-radius: 8px;
@@ -570,9 +628,28 @@
   border-radius: 8px;
 }
 
-td.annodel {
+.ficheannotations table#etudannotations {
+    width: 100%;
+    border-collapse: collapse;
 }
+.ficheannotations table#etudannotations tr:nth-child(odd) {
+    background: rgb(240, 240, 240);
+}
+.ficheannotations table#etudannotations tr:nth-child(even) {
+    background: rgb(230, 230, 230);
+}
 
+.ficheannotations span.annodate {
+    color: rgb(200, 50, 50);
+    font-size: 80%;
+}
+.ficheannotations span.annoc {
+    color: navy;
+}
+.ficheannotations td.annodel {
+    text-align: right;
+}
+
 span.link_bul_pdf {
   font-size: 80%;
 }
@@ -2047,7 +2124,7 @@
 }
 
 .recap_parcours tr.sem_courant {
-  background-color: rgb(245, 243, 116);
+    background-color: rgb(255, 241, 118);
 }
 
 .recap_parcours tr.sem_precedent {

Modified: branches/ScoDoc7/static/js/scodoc.js
===================================================================
--- branches/ScoDoc7/static/js/scodoc.js	2017-12-05 13:25:09 UTC (rev 1724)
+++ branches/ScoDoc7/static/js/scodoc.js	2017-12-11 10:44:54 UTC (rev 1725)
@@ -15,7 +15,7 @@
         });    
     
     // Date picker
-	$(".datepicker").datepicker({
+    $(".datepicker").datepicker({
         showOn: 'button', 
         buttonImage: '/ScoDoc/static/icons/calendar_img.png', 
         buttonImageOnly: true,
@@ -108,3 +108,13 @@
 });
 
 
+// Show tags (readonly)
+function readOnlyTags(nodes) {
+    // nodes are textareas, hide them and create a span showing tags
+    for (var i = 0; i < nodes.length; i++) {
+	var node = $(nodes[i]);
+	node.hide();
+	var tags = nodes[i].value.split(',');
+	node.after('<span class="ro_tags"><span class="ro_tag">' + tags.join('</span><span class="ro_tag">') + '</span></span>');
+    }
+}


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