backport 3.5 step 1, remaining wf changes in hooks to merge
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 26 Aug 2009 14:45:56 +0200
changeset 3023 7864fee8b4ec
parent 2968 0e3460341023 (current diff)
parent 3022 238ad682bcb7 (diff)
child 3024 bfaf056f1029
backport 3.5 step 1, remaining wf changes in hooks to merge
common/mixins.py
common/uilib.py
entities/test/unittest_wfobjs.py
entity.py
goa/appobjects/components.py
hooks/notification.py
rset.py
selectors.py
server/migractions.py
server/serverctl.py
sobjects/notification.py
test/unittest_rset.py
view.py
web/component.py
web/data/cubicweb.formfilter.js
web/facet.py
web/form.py
web/test/unittest_form.py
web/views/basecomponents.py
web/views/basetemplates.py
web/views/baseviews.py
web/views/editcontroller.py
web/views/editviews.py
web/views/emailaddress.py
web/views/facets.py
web/views/formrenderers.py
web/views/ibreadcrumbs.py
web/views/iprogress.py
web/views/management.py
web/views/navigation.py
web/views/old_calendar.py
web/views/startup.py
web/views/tableview.py
--- a/common/mixins.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/common/mixins.py	Wed Aug 26 14:45:56 2009 +0200
@@ -251,7 +251,7 @@
     """a recursive path view"""
     id = 'path'
     item_vid = 'oneline'
-    separator = u'&nbsp;&gt;&nbsp;'
+    separator = u'&#160;&gt;&#160;'
 
     def call(self, **kwargs):
         self.w(u'<div class="pathbar">')
--- a/common/test/unittest_uilib.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/common/test/unittest_uilib.py	Wed Aug 26 14:45:56 2009 +0200
@@ -81,8 +81,6 @@
             got = uilib.text_cut(text, 30)
             self.assertEquals(got, expected)
 
-
-
 if __name__ == '__main__':
     unittest_main()
 
--- a/common/uilib.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/common/uilib.py	Wed Aug 26 14:45:56 2009 +0200
@@ -263,7 +263,6 @@
         res = unicode(res, 'UTF8')
     return res
 
-
 # traceback formatting ########################################################
 
 import traceback
@@ -309,7 +308,7 @@
             xml_escape(stackentry[0]), stackentry[1], xml_escape(stackentry[2])))
         if stackentry[3]:
             string = xml_escape(stackentry[3]).decode('utf-8', 'replace')
-            strings.append(u'&nbsp;&nbsp;%s<br/>\n' % (string))
+            strings.append(u'&#160;&#160;%s<br/>\n' % (string))
         # add locals info for each entry
         try:
             local_context = tcbk.tb_frame.f_locals
--- a/debian/control	Fri Aug 21 16:26:20 2009 +0200
+++ b/debian/control	Wed Aug 26 14:45:56 2009 +0200
@@ -76,7 +76,7 @@
 Package: cubicweb-common
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.44.0), python-yams (>= 0.24.0), python-rql (>= 0.22.1), python-lxml
+Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.44.0), python-yams (>= 0.24.0), python-rql (>= 0.22.3), python-lxml
 Recommends: python-simpletal (>= 4.0)
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/debian/cubicweb-ctl.cubicweb.init	Fri Aug 21 16:26:20 2009 +0200
+++ b/debian/cubicweb-ctl.cubicweb.init	Wed Aug 26 14:45:56 2009 +0200
@@ -22,11 +22,14 @@
 # Check if we are sure to not want the start-stop-daemon machinery here
 # Refer to Debian Policy Manual section 9.3.2 (Writing the scripts) for details.
 
-case "$1" in
-	"force-reload")
-		/usr/bin/cubicweb-ctl reload --force
-		;;
-	"*|restart")
-		/usr/bin/cubicweb-ctl $1 --force
-		;;
+case $1 in
+    force-reload)
+        /usr/bin/cubicweb-ctl reload --force
+        ;;
+    status)
+        /usr/bin/cubicweb-ctl status
+        ;;
+    *)
+        /usr/bin/cubicweb-ctl $1 --force
+        ;;
 esac
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/dataimport.py	Wed Aug 26 14:45:56 2009 +0200
@@ -0,0 +1,278 @@
+# -*- coding: utf-8 -*-
+"""This module provides tools to import tabular data.
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+
+
+Example of use (run this with `cubicweb-ctl shell instance import-script.py`):
+
+.. sourcecode:: python
+
+  # define data generators
+  GENERATORS = []
+
+  USERS = [('Prenom', 'firstname', ()),
+           ('Nom', 'surname', ()),
+           ('Identifiant', 'login', ()),
+           ]
+
+  def gen_users(ctl):
+      for row in ctl.get_data('utilisateurs'):
+          entity = mk_entity(row, USERS)
+          entity['upassword'] = u'motdepasse'
+          ctl.check('login', entity['login'], None)
+          ctl.store.add('CWUser', entity)
+          email = {'address': row['email']}
+          ctl.store.add('EmailAddress', email)
+          ctl.store.relate(entity['eid'], 'use_email', email['eid'])
+          ctl.store.rql('SET U in_group G WHERE G name "users", U eid %(x)s', {'x':entity['eid']})
+
+  CHK = [('login', check_doubles, 'Utilisateurs Login',
+          'Deux utilisateurs ne devraient pas avoir le même login.'),
+         ]
+
+  GENERATORS.append( (gen_users, CHK) )
+
+  # progress callback
+  def tell(msg):
+      print msg
+
+  # create controller
+  ctl = CWImportController(RQLObjectStore())
+  ctl.askerror = True
+  ctl._tell = tell
+  ctl.generators = GENERATORS
+  ctl.store._checkpoint = checkpoint
+  ctl.store._rql = rql
+  ctl.data['utilisateurs'] = lazytable(utf8csvreader(open('users.csv')))
+  # run
+  ctl.run()
+  sys.exit(0)
+
+"""
+__docformat__ = "restructuredtext en"
+
+import sys, csv, traceback
+
+from logilab.common import shellutils
+
+def utf8csvreader(file, encoding='utf-8', separator=',', quote='"'):
+    """A csv reader that accepts files with any encoding and outputs
+    unicode strings."""
+    for row in csv.reader(file, delimiter=separator, quotechar=quote):
+        yield [item.decode(encoding) for item in row]
+
+def lazytable(reader):
+    """The first row is taken to be the header of the table and
+    used to output a dict for each row of data.
+
+    >>> data = lazytable(utf8csvreader(open(filename)))
+    """
+    header = reader.next()
+    for row in reader:
+        yield dict(zip(header, row))
+
+# base sanitizing functions #####
+
+def capitalize_if_unicase(txt):
+    if txt.isupper() or txt.islower():
+        return txt.capitalize()
+    return txt
+
+def no_space(txt):
+    return txt.replace(' ','')
+
+def no_uspace(txt):
+    return txt.replace(u'\xa0','')
+
+def no_dash(txt):
+    return txt.replace('-','')
+
+def alldigits(txt):
+    if txt.isdigit():
+        return txt
+    else:
+        return u''
+
+def strip(txt):
+    return txt.strip()
+
+# base checks #####
+
+def check_doubles(buckets):
+    """Extract the keys that have more than one item in their bucket."""
+    return [(key, len(value)) for key,value in buckets.items() if len(value) > 1]
+
+# make entity helper #####
+
+def mk_entity(row, map):
+    """Return a dict made from sanitized mapped values.
+
+    >>> row = {'myname': u'dupont'}
+    >>> map = [('myname', u'name', (capitalize_if_unicase,))]
+    >>> mk_entity(row, map)
+    {'name': u'Dupont'}
+    """
+    res = {}
+    for src, dest, funcs in map:
+        res[dest] = row[src]
+        for func in funcs:
+            res[dest] = func(res[dest])
+    return res
+
+# object stores
+
+class ObjectStore(object):
+    """Store objects in memory for faster testing. Will not
+    enforce the constraints of the schema and hence will miss
+    some problems.
+
+    >>> store = ObjectStore()
+    >>> user = {'login': 'johndoe'}
+    >>> store.add('CWUser', user)
+    >>> group = {'name': 'unknown'}
+    >>> store.add('CWUser', group)
+    >>> store.relate(user['eid'], 'in_group', group['eid'])
+    """
+
+    def __init__(self):
+        self.items = []
+        self.eids = {}
+        self.types = {}
+        self.relations = set()
+        self.indexes = {}
+        self._rql = None
+        self._checkpoint = None
+
+    def _put(self, type, item):
+        self.items.append(item)
+        return len(self.items) - 1
+
+    def add(self, type, item):
+        assert isinstance(item, dict), item
+        eid = item['eid'] = self._put(type, item)
+        self.eids[eid] = item
+        self.types.setdefault(type, []).append(eid)
+
+    def relate(self, eid_from, rtype, eid_to):
+        eids_valid = (eid_from < len(self.items) and eid_to <= len(self.items))
+        assert eids_valid, 'eid error %s %s' % (eid_from, eid_to)
+        self.relations.add( (eid_from, rtype, eid_to) )
+
+    def build_index(self, name, type, func):
+        index = {}
+        for eid in self.types[type]:
+            index.setdefault(func(self.eids[eid]), []).append(eid)
+        self.indexes[name] = index
+
+    def get_many(self, name, key):
+        return self.indexes[name].get(key, [])
+
+    def get_one(self, name, key):
+        eids = self.indexes[name].get(key, [])
+        assert len(eids) == 1
+        return eids[0]
+
+    def find(self, type, key, value):
+        for idx in self.types[type]:
+            item = self.items[idx]
+            if item[key] == value:
+                yield item
+
+    def rql(self, query, args):
+        if self._rql:
+            return self._rql(query, args)
+
+    def checkpoint(self):
+        if self._checkpoint:
+            self._checkpoint()
+
+class RQLObjectStore(ObjectStore):
+    """ObjectStore that works with an actual RQL repository."""
+
+    def _put(self, type, item):
+        query = ('INSERT %s X: ' % type) + ', '.join(['X %s %%(%s)s' % (key,key) for key in item])
+        return self.rql(query, item)[0][0]
+
+    def relate(self, eid_from, rtype, eid_to):
+        query = 'SET X %s Y WHERE X eid %%(from)s, Y eid %%(to)s' % rtype
+        self.rql(query, {'from': int(eid_from), 'to': int(eid_to)})
+        self.relations.add( (eid_from, rtype, eid_to) )
+
+# import controller #####
+
+class CWImportController(object):
+    """Controller of the data import process.
+
+    >>> ctl = CWImportController(store)
+    >>> ctl.generators = list_of_data_generators
+    >>> ctl.data = dict_of_data_tables
+    >>> ctl.run()
+    """
+
+    def __init__(self, store):
+        self.store = store
+        self.generators = None
+        self.data = {}
+        self.errors = None
+        self.askerror = False
+
+    def check(self, type, key, value):
+        self._checks.setdefault(type, {}).setdefault(key, []).append(value)
+
+    def check_map(self, entity, key, map, default):
+        try:
+            entity[key] = map[entity[key]]
+        except KeyError:
+            self.check(key, entity[key], None)
+            entity[key] = default
+
+    def run(self):
+        self.errors = {}
+        for func, checks in self.generators:
+            self._checks = {}
+            func_name = func.__name__[4:]
+            question = 'Importation de %s' % func_name
+            self.tell(question)
+            try:
+                func(self)
+            except:
+                import StringIO
+                tmp = StringIO.StringIO()
+                traceback.print_exc(file=tmp)
+                print tmp.getvalue()
+                self.errors[func_name] = ('Erreur lors de la transformation',
+                                          tmp.getvalue().splitlines())
+            for key, func, title, help in checks:
+                buckets = self._checks.get(key)
+                if buckets:
+                    err = func(buckets)
+                    if err:
+                        self.errors[title] = (help, err)
+            self.store.checkpoint()
+        errors = sum(len(err[1]) for err in self.errors.values())
+        self.tell('Importation terminée. (%i objets, %i types, %i relations et %i erreurs).'
+                  % (len(self.store.eids), len(self.store.types),
+                     len(self.store.relations), errors))
+        if self.errors and self.askerror and confirm('Afficher les erreurs ?'):
+            import pprint
+            pprint.pprint(self.errors)
+
+    def get_data(self, key):
+        return self.data.get(key)
+
+    def index(self, name, key, value):
+        self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value)
+
+    def tell(self, msg):
+        self._tell(msg)
+
+def confirm(question):
+    """A confirm function that asks for yes/no/abort and exits on abort."""
+    answer = shellutils.ASK.ask(question, ('Y','n','abort'), 'Y')
+    if answer == 'abort':
+        sys.exit(1)
+    return answer == 'Y'
--- a/entities/test/unittest_wfobjs.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/entities/test/unittest_wfobjs.py	Wed Aug 26 14:45:56 2009 +0200
@@ -1,12 +1,15 @@
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import ValidationError
 
-def add_wf(self, etype, name=None):
+def add_wf(self, etype, name=None, default=False):
     if name is None:
-        name = unicode(etype)
-    wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': name}).get_entity(0, 0)
+        name = etype
+    wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': unicode(name)}).get_entity(0, 0)
     self.execute('SET WF workflow_of ET WHERE WF eid %(wf)s, ET name %(et)s',
                  {'wf': wf.eid, 'et': etype})
+    if default:
+        self.execute('SET ET default_workflow WF WHERE WF eid %(wf)s, ET name %(et)s',
+                     {'wf': wf.eid, 'et': etype})
     return wf
 
 def parse_hist(wfhist):
@@ -31,10 +34,15 @@
     def test_duplicated_state(self):
         wf = add_wf(self, 'Company')
         wf.add_state(u'foo', initial=True)
+        self.commit()
         wf.add_state(u'foo')
         ex = self.assertRaises(ValidationError, self.commit)
         # XXX enhance message
         self.assertEquals(ex.errors, {'state_of': 'unique constraint S name N, Y state_of O, Y name N failed'})
+        # no pb if not in the same workflow
+        wf2 = add_wf(self, 'Company')
+        foo = wf2.add_state(u'foo', initial=True)
+        self.commit()
 
     def test_duplicated_transition(self):
         wf = add_wf(self, 'Company')
@@ -98,9 +106,20 @@
         trinfo = self._test_manager_deactivate(user)
         self.assertEquals(trinfo.transition, None)
 
+    def test_set_in_state_bad_wf(self):
+        wf = add_wf(self, 'CWUser')
+        s = wf.add_state(u'foo', initial=True)
+        self.commit()
+        ex = self.assertRaises(ValidationError, self.session().unsafe_execute,
+                               'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
+                               {'x': self.user().eid, 's': s.eid}, 'x')
+        self.assertEquals(ex.errors, {'in_state': "state doesn't belong to entity's workflow. "
+                                      "You may want to set a custom workflow for this entity first."})
+
     def test_fire_transition(self):
         user = self.user()
         user.fire_transition('deactivate', comment=u'deactivate user')
+        user.clear_all_caches()
         self.assertEquals(user.state, 'deactivated')
         self._test_manager_deactivate(user)
         trinfo = self._test_manager_deactivate(user)
@@ -139,64 +158,102 @@
                      'WHERE T name "deactivate"')
         self._test_stduser_deactivate()
 
-    def _init_wf_with_shared_state_or_tr(self):
-        req = self.request()
-        etypes = dict(self.execute('Any N, ET WHERE ET is CWEType, ET name N'
-                                   ', ET name IN ("CWGroup", "Bookmark")'))
-        self.grpwf = req.create_entity('Workflow', ('workflow_of', 'ET'),
-                                       ET=etypes['CWGroup'], name=u'group workflow')
-        self.bmkwf = req.execute('Any X WHERE X is Workflow, X workflow_of ET, ET name "Bookmark"').get_entity(0, 0)
-        self.state1 = self.grpwf.add_state(u'state1', initial=True)
-        self.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s',
-                     {'s': self.state1.eid, 'wf': self.bmkwf.eid})
-        self.execute('SET WF initial_state S WHERE S eid %(s)s, WF eid %(wf)s',
-                     {'s': self.state1.eid, 'wf': self.bmkwf.eid})
-        self.state2 = self.grpwf.add_state(u'state2')
-        self.group = self.add_entity('CWGroup', name=u't1')
-        self.bookmark = self.add_entity('Bookmark', title=u'111', path=u'/view')
-        # commit to link to the initial state
-        self.commit()
+    def test_subworkflow_base(self):
+        """subworkflow
 
-    def test_transitions_selection(self):
-        """
-        ------------------------  tr1    -----------------
-        | state1 (CWGroup, Bookmark) | ------> | state2 (CWGroup) |
-        ------------------------         -----------------
-                  |  tr2    ------------------
-                  `------>  | state3 (Bookmark) |
-                            ------------------
+        +-----------+  tr1   +-----------+
+        | swfstate1 | ------>| swfstate2 |
+        +-----------+        +-----------+
+                  |  tr2  +-----------+
+                  `------>| swfstate3 |
+                          +-----------+
+
+        main workflow
+
+        +--------+  swftr1             +--------+
+        | state1 | -------[swfstate2]->| state2 |
+        +--------+     |               +--------+
+                       |               +--------+
+                       `-[swfstate3]-->| state3 |
+                                       +--------+
         """
-        self._init_wf_with_shared_state_or_tr()
-        state3 = self.bmkwf.add_state(u'state3')
-        tr1 = self.grpwf.add_transition(u'tr1', (self.state1,), self.state2)
-        tr2 = self.bmkwf.add_transition(u'tr2', (self.state1,), state3)
-        transitions = list(self.group.possible_transitions())
-        self.assertEquals(len(transitions), 1)
-        self.assertEquals(transitions[0].name, 'tr1')
-        transitions = list(self.bookmark.possible_transitions())
-        self.assertEquals(len(transitions), 1)
-        self.assertEquals(transitions[0].name, 'tr2')
-
+        # sub-workflow
+        swf = add_wf(self, 'CWGroup', name='subworkflow')
+        swfstate1 = swf.add_state(u'swfstate1', initial=True)
+        swfstate2 = swf.add_state(u'swfstate2')
+        swfstate3 = swf.add_state(u'swfstate3')
+        tr1 = swf.add_transition(u'tr1', (swfstate1,), swfstate2)
+        tr2 = swf.add_transition(u'tr2', (swfstate1,), swfstate3)
+        # main workflow
+        mwf = add_wf(self, 'CWGroup', name='main workflow', default=True)
+        state1 = mwf.add_state(u'state1', initial=True)
+        state2 = mwf.add_state(u'state2')
+        state3 = mwf.add_state(u'state3')
+        swftr1 = mwf.add_wftransition(u'swftr1', swf, state1,
+                                      [(swfstate2, state2), (swfstate3, state3)])
+        self.assertEquals(swftr1.destination().eid, swfstate1.eid)
+        # workflows built, begin test
+        self.group = self.add_entity('CWGroup', name=u'grp1')
+        self.commit()
+        self.assertEquals(self.group.current_state.eid, state1.eid)
+        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.subworkflow_input_transition(), None)
+        self.group.fire_transition('swftr1', u'go')
+        self.commit()
+        self.group.clear_all_caches()
+        self.assertEquals(self.group.current_state.eid, swfstate1.eid)
+        self.assertEquals(self.group.current_workflow.eid, swf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.subworkflow_input_transition().eid, swftr1.eid)
+        self.group.fire_transition('tr1', u'go')
+        self.commit()
+        self.group.clear_all_caches()
+        self.assertEquals(self.group.current_state.eid, state2.eid)
+        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.subworkflow_input_transition(), None)
+        # force back to swfstate1 is impossible since we can't any more find
+        # subworkflow input transition
+        ex = self.assertRaises(ValidationError,
+                               self.group.change_state, swfstate1, u'gadget')
+        self.assertEquals(ex.errors, {'to_state': "state doesn't belong to entity's current workflow"})
+        self.rollback()
+        # force back to state1
+        self.group.change_state('state1', u'gadget')
+        self.group.fire_transition('swftr1', u'au')
+        self.group.clear_all_caches()
+        self.group.fire_transition('tr2', u'chapeau')
+        self.commit()
+        self.group.clear_all_caches()
+        self.assertEquals(self.group.current_state.eid, state3.eid)
+        self.assertEquals(self.group.current_workflow.eid, mwf.eid)
+        self.assertEquals(self.group.main_workflow.eid, mwf.eid)
+        self.assertListEquals(parse_hist(self.group.workflow_history),
+                              [('state1', 'swfstate1', 'swftr1', 'go'),
+                               ('swfstate1', 'swfstate2', 'tr1', 'go'),
+                               ('swfstate2', 'state2', 'swftr1', 'exiting from subworkflow subworkflow'),
+                               ('state2', 'state1', None, 'gadget'),
+                               ('state1', 'swfstate1', 'swftr1', 'au'),
+                               ('swfstate1', 'swfstate3', 'tr2', 'chapeau'),
+                               ('swfstate3', 'state3', 'swftr1', 'exiting from subworkflow subworkflow'),
+                               ])
 
-    def test_transitions_selection2(self):
-        """
-        ------------------------  tr1 (Bookmark)   -----------------------
-        | state1 (CWGroup, Bookmark) | -------------> | state2 (CWGroup,Bookmark) |
-        ------------------------                -----------------------
-                  |  tr2 (CWGroup)                     |
-                  `---------------------------------/
-        """
-        self._init_wf_with_shared_state_or_tr()
-        self.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s',
-                     {'s': self.state2.eid, 'wf': self.bmkwf.eid})
-        tr1 = self.bmkwf.add_transition(u'tr1', (self.state1,), self.state2)
-        tr2 = self.grpwf.add_transition(u'tr2', (self.state1,), self.state2)
-        transitions = list(self.group.possible_transitions())
-        self.assertEquals(len(transitions), 1)
-        self.assertEquals(transitions[0].name, 'tr2')
-        transitions = list(self.bookmark.possible_transitions())
-        self.assertEquals(len(transitions), 1)
-        self.assertEquals(transitions[0].name, 'tr1')
+    def test_subworkflow_exit_consistency(self):
+        # sub-workflow
+        swf = add_wf(self, 'CWGroup', name='subworkflow')
+        swfstate1 = swf.add_state(u'swfstate1', initial=True)
+        swfstate2 = swf.add_state(u'swfstate2')
+        tr1 = swf.add_transition(u'tr1', (swfstate1,), swfstate2)
+        # main workflow
+        mwf = add_wf(self, 'CWGroup', name='main workflow', default=True)
+        state1 = mwf.add_state(u'state1', initial=True)
+        state2 = mwf.add_state(u'state2')
+        state3 = mwf.add_state(u'state3')
+        mwf.add_wftransition(u'swftr1', swf, state1,
+                             [(swfstate2, state2), (swfstate2, state3)])
+        ex = self.assertRaises(ValidationError, self.commit)
+        self.assertEquals(ex.errors, {'subworkflow_exit': u"can't have multiple exits on the same state"})
 
 
 class CustomWorkflowTC(CubicWebTC):
@@ -239,25 +296,6 @@
                            ('deactivated', 'activated', 'activate', None),
                            ('activated', 'asleep', None, 'workflow changed to "CWUser"')])
 
-    def test_custom_wf_shared_state(self):
-        """member in some state shared by the new workflow, nothing has to be
-        done
-        """
-        self.member.fire_transition('deactivate')
-        self.assertEquals(self.member.state, 'deactivated')
-        wf = add_wf(self, 'CWUser')
-        wf.add_state('asleep', initial=True)
-        self.execute('SET S state_of WF WHERE S name "deactivated", WF eid %(wf)s',
-                     {'wf': wf.eid})
-        self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
-                     {'wf': wf.eid, 'x': self.member.eid})
-        self.commit()
-        self.member.clear_all_caches()
-        self.assertEquals(self.member.current_workflow.eid, wf.eid)
-        self.assertEquals(self.member.state, 'deactivated')
-        self.assertEquals(parse_hist(self.member.workflow_history),
-                          [('activated', 'deactivated', 'deactivate', None)])
-
     def test_custom_wf_no_initial_state(self):
         """try to set a custom workflow which has no initial state"""
         self.member.fire_transition('deactivate')
@@ -269,8 +307,7 @@
         self.assertEquals(ex.errors, {'custom_workflow': u'workflow has no initial state'})
 
     def test_custom_wf_bad_etype(self):
-        """try to set a custom workflow which has no initial state"""
-        self.member.fire_transition('deactivate')
+        """try to set a custom workflow which doesn't apply to entity type"""
         wf = add_wf(self, 'Company')
         wf.add_state('asleep', initial=True)
         self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
--- a/entities/wfobjs.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/entities/wfobjs.py	Wed Aug 26 14:45:56 2009 +0200
@@ -16,6 +16,7 @@
 from cubicweb.interfaces import IWorkflowable
 from cubicweb.common.mixins import MI_REL_TRIGGERS
 
+class WorkflowException(Exception): pass
 
 class Workflow(AnyEntity):
     id = 'Workflow'
@@ -40,6 +41,23 @@
             return self.workflow_of[0].rest_path(), {'vid': 'workflow'}
         return super(Workflow, self).after_deletion_path()
 
+    def iter_workflows(self, _done=None):
+        """return an iterator on actual workflows, eg this workflow and its
+        subworkflows
+        """
+        # infinite loop safety belt
+        if _done is None:
+            _done = set()
+        yield self
+        _done.add(self.eid)
+        for tr in self.req.execute('Any T WHERE T is WorkflowTransition, '
+                                   'T transition_of WF, WF eid %(wf)s',
+                                   {'wf': self.eid}).entities():
+            if tr.subwf.eid in _done:
+                continue
+            for subwf in tr.subwf.iter_workflows(_done):
+                yield subwf
+
     # state / transitions accessors ############################################
 
     def state_by_name(self, statename):
@@ -77,9 +95,7 @@
     # wf construction methods ##################################################
 
     def add_state(self, name, initial=False, **kwargs):
-        """method to ease workflow definition: add a state for one or more
-        entity type(s)
-        """
+        """add a state to this workflow"""
         state = self.req.create_entity('State', name=unicode(name), **kwargs)
         self.req.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s',
                          {'s': state.eid, 'wf': self.eid}, ('s', 'wf'))
@@ -90,27 +106,47 @@
                              {'s': state.eid, 'wf': self.eid}, ('s', 'wf'))
         return state
 
-    def add_transition(self, name, fromstates, tostate,
-                       requiredgroups=(), conditions=(), **kwargs):
-        """method to ease workflow definition: add a transition for one or more
-        entity type(s), from one or more state and to a single state
-        """
-        tr = self.req.create_entity('Transition', name=unicode(name), **kwargs)
+    def _add_transition(self, trtype, name, fromstates,
+                        requiredgroups=(), conditions=(), **kwargs):
+        tr = self.req.create_entity(trtype, name=unicode(name), **kwargs)
         self.req.execute('SET T transition_of WF '
                          'WHERE T eid %(t)s, WF eid %(wf)s',
                          {'t': tr.eid, 'wf': self.eid}, ('t', 'wf'))
+        assert fromstates, fromstates
+        if not isinstance(fromstates, (tuple, list)):
+            fromstates = (fromstates,)
         for state in fromstates:
             if hasattr(state, 'eid'):
                 state = state.eid
             self.req.execute('SET S allowed_transition T '
                              'WHERE S eid %(s)s, T eid %(t)s',
                              {'s': state, 't': tr.eid}, ('s', 't'))
+        tr.set_transition_permissions(requiredgroups, conditions, reset=False)
+        return tr
+
+    def add_transition(self, name, fromstates, tostate,
+                       requiredgroups=(), conditions=(), **kwargs):
+        """add a transition to this workflow from some state(s) to another"""
+        tr = self._add_transition('Transition', name, fromstates,
+                                  requiredgroups, conditions, **kwargs)
         if hasattr(tostate, 'eid'):
             tostate = tostate.eid
         self.req.execute('SET T destination_state S '
                          'WHERE S eid %(s)s, T eid %(t)s',
                          {'t': tr.eid, 's': tostate}, ('s', 't'))
-        tr.set_transition_permissions(requiredgroups, conditions, reset=False)
+        return tr
+
+    def add_wftransition(self, name, subworkflow, fromstates, exitpoints,
+                       requiredgroups=(), conditions=(), **kwargs):
+        """add a workflow transition to this workflow"""
+        tr = self._add_transition('WorkflowTransition', name, fromstates,
+                                  requiredgroups, conditions, **kwargs)
+        if hasattr(subworkflow, 'eid'):
+            subworkflow = subworkflow.eid
+        self.req.execute('SET T subworkflow WF WHERE WF eid %(wf)s,T eid %(t)s',
+                         {'t': tr.eid, 'wf': subworkflow}, ('wf', 't'))
+        for fromstate, tostate in exitpoints:
+            tr.add_exit_point(fromstate, tostate)
         return tr
 
 
@@ -125,9 +161,18 @@
 
     def __init__(self, *args, **kwargs):
         if self.id == 'BaseTransition':
-            raise Exception('should not be instantiated')
+            raise WorkflowException('should not be instantiated')
         super(BaseTransition, self).__init__(*args, **kwargs)
 
+    @property
+    def workflow(self):
+        return self.transition_of[0]
+
+    def has_input_state(self, state):
+        if hasattr(state, 'eid'):
+            state = state.eid
+        return any(s for s in self.reverse_allowed_transition if s.eid == state)
+
     def may_be_fired(self, eid):
         """return true if the logged user may fire this transition
 
@@ -170,7 +215,6 @@
             self.req.execute('DELETE T condition R WHERE T eid %(x)s',
                              {'x': self.eid}, 'x')
         for gname in requiredgroups:
-            ### XXX ensure gname validity
             rset = self.req.execute('SET T require_group G '
                                     'WHERE T eid %(x)s, G name %(gn)s',
                                     {'x': self.eid, 'gn': gname}, 'x')
@@ -194,11 +238,6 @@
     def destination(self):
         return self.destination_state[0]
 
-    def has_input_state(self, state):
-        if hasattr(state, 'eid'):
-            state = state.eid
-        return any(s for s in self.reverse_allowed_transition if s.eid == state)
-
 
 class WorkflowTransition(BaseTransition):
     """customized class for WorkflowTransition entities"""
@@ -211,6 +250,50 @@
     def destination(self):
         return self.subwf.initial
 
+    def add_exit_point(self, fromstate, tostate):
+        if hasattr(fromstate, 'eid'):
+            fromstate = fromstate.eid
+        if hasattr(tostate, 'eid'):
+            tostate = tostate.eid
+        self.req.execute('INSERT SubWorkflowExitPoint X: T subworkflow_exit X, '
+                         'X subworkflow_state FS, X destination_state TS '
+                         'WHERE T eid %(t)s, FS eid %(fs)s, TS eid %(ts)s',
+                         {'t': self.eid, 'fs': fromstate, 'ts': tostate},
+                         ('t', 'fs', 'ts'))
+
+    def get_exit_point(self, state):
+        """if state is an exit point, return its associated destination state"""
+        if hasattr(state, 'eid'):
+            state = state.eid
+        stateeid = self.exit_points().get(state)
+        if stateeid is not None:
+            return self.req.entity_from_eid(stateeid)
+        return None
+
+    @cached
+    def exit_points(self):
+        result = {}
+        for ep in self.subworkflow_exit:
+            result[ep.subwf_state.eid] = ep.destination.eid
+        return result
+
+    def clear_all_caches(self):
+        super(WorkflowableMixIn, self).clear_all_caches()
+        clear_cache(self, 'exit_points')
+
+
+class SubWorkflowExitPoint(AnyEntity):
+    """customized class for SubWorkflowExitPoint entities"""
+    id = 'SubWorkflowExitPoint'
+
+    @property
+    def subwf_state(self):
+        return self.subworkflow_state[0]
+
+    @property
+    def destination(self):
+        return self.destination_state[0]
+
 
 class State(AnyEntity):
     """customized class for State entities"""
@@ -218,6 +301,10 @@
     fetch_attrs, fetch_order = fetch_config(['name'])
     rest_attr = 'eid'
 
+    @property
+    def workflow(self):
+        return self.state_of[0]
+
     def after_deletion_path(self):
         """return (path, parameters) which should be used as redirect
         information when this entity is being deleted
@@ -266,13 +353,18 @@
     __implements__ = (IWorkflowable,)
 
     @property
-    def current_workflow(self):
+    def main_workflow(self):
         """return current workflow applied to this entity"""
         if self.custom_workflow:
             return self.custom_workflow[0]
         return self.cwetype_workflow()
 
     @property
+    def current_workflow(self):
+        """return current workflow applied to this entity"""
+        return self.current_state and self.current_state.workflow or self.main_workflow
+
+    @property
     def current_state(self):
         """return current state entity"""
         return self.in_state and self.in_state[0] or None
@@ -337,13 +429,21 @@
             if tr.may_be_fired(self.eid):
                 yield tr
 
-    def _get_tr_kwargs(self, comment, commentformat):
+    def _add_trinfo(self, comment, commentformat, treid=None, tseid=None):
         kwargs = {}
         if comment is not None:
             kwargs['comment'] = comment
             if commentformat is not None:
                 kwargs['comment_format'] = commentformat
-        return kwargs
+        args = [('wf_info_for', 'E')]
+        kwargs['E'] = self.eid
+        if treid is not None:
+            args.append( ('by_transition', 'T') )
+            kwargs['T'] = treid
+        if tseid is not None:
+            args.append( ('to_state', 'S') )
+            kwargs['S'] = tseid
+        return self.req.create_entity('TrInfo', *args, **kwargs)
 
     def fire_transition(self, trname, comment=None, commentformat=None):
         """change the entity's state by firing transition of the given name in
@@ -351,30 +451,51 @@
         """
         assert self.current_workflow
         tr = self.current_workflow.transition_by_name(trname)
-        assert tr is not None, 'not a %s transition: %s' % (self.id, state)
-        # XXX try to find matching transition?
-        self.req.create_entity('TrInfo', ('by_transition', 'T'),
-                               ('wf_info_for', 'E'), T=tr.eid, E=self.eid,
-                               **self._get_tr_kwargs(comment, commentformat))
+        assert tr is not None, 'not a %s transition: %s' % (self.id, trname)
+        return self._add_trinfo(comment, commentformat, tr.eid)
 
-    def change_state(self, statename, comment=None, commentformat=None):
-        """change the entity's state to the state of the given name in entity's
-        workflow. This method should only by used by manager to fix an entity's
-        state when their is no matching transition, otherwise fire_transition
-        should be used.
+    def change_state(self, statename, comment=None, commentformat=None, tr=None):
+        """change the entity's state to the given state (name or entity) in
+        entity's workflow. This method should only by used by manager to fix an
+        entity's state when their is no matching transition, otherwise
+        fire_transition should be used.
         """
         assert self.current_workflow
-        if not isinstance(statename, basestring):
-            warn('give a state name')
-            state = self.current_workflow.state_by_eid(statename)
-            assert state is not None, 'not a %s state: %s' % (self.id, state)
+        if hasattr(statename, 'eid'):
+            stateeid = statename.eid
         else:
-            state = self.current_workflow.state_by_name(statename)
+            if not isinstance(statename, basestring):
+                warn('give a state name')
+                state = self.current_workflow.state_by_eid(statename)
+            else:
+                state = self.current_workflow.state_by_name(statename)
+            if state is None:
+                raise WorkflowException('not a %s state: %s' % (self.id,
+                                                                statename))
+            stateeid = state.eid
         # XXX try to find matching transition?
-        self.req.create_entity('TrInfo', ('to_state', 'S'),
-                               ('wf_info_for', 'E'), S=state.eid, E=self.eid,
-                               **self._get_tr_kwargs(comment, commentformat))
+        return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid)
 
+    def subworkflow_input_transition(self):
+        """return the transition which has went through the current sub-workflow
+        """
+        if self.main_workflow.eid == self.current_workflow.eid:
+            return # doesn't make sense
+        subwfentries = []
+        for trinfo in reversed(self.workflow_history):
+            if (trinfo.transition and
+                trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid):
+                # entering or leaving a subworkflow
+                if (subwfentries and
+                    subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid):
+                    # leave
+                    del subwfentries[-1]
+                else:
+                    # enter
+                    subwfentries.append(trinfo)
+        if not subwfentries:
+            return None
+        return subwfentries[-1].transition
 
     def clear_all_caches(self):
         super(WorkflowableMixIn, self).clear_all_caches()
--- a/entity.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/entity.py	Wed Aug 26 14:45:56 2009 +0200
@@ -209,6 +209,9 @@
     def __hash__(self):
         return id(self)
 
+    def __cmp__(self):
+        raise NotImplementedError('comparison not implemented for %s' % self.__class__)
+
     def pre_add_hook(self):
         """hook called by the repository before doing anything to add the entity
         (before_add entity hooks have not been called yet). This give the
--- a/goa/appobjects/components.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/goa/appobjects/components.py	Wed Aug 26 14:45:56 2009 +0200
@@ -74,7 +74,7 @@
         label = display_name(req, etype, 'plural')
         view = self.vreg.select('views', 'list', req, req.etype_rset(etype))
         url = view.url()
-        etypelink = u'&nbsp;<a href="%s">%s</a>' % (xml_escape(url), label)
+        etypelink = u'&#160;<a href="%s">%s</a>' % (xml_escape(url), label)
         yield (label, etypelink, self.add_entity_link(eschema, req))
 
 ManageView.entity_types = entity_types_no_count
--- a/goa/appobjects/gauthservice.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/goa/appobjects/gauthservice.py	Wed Aug 26 14:45:56 2009 +0200
@@ -17,7 +17,7 @@
 
     def anon_user_link(self):
         self.w(self.req._('anonymous'))
-        self.w(u'&nbsp;[<a class="logout" href="%s">%s</a>]'
+        self.w(u'&#160;[<a class="logout" href="%s">%s</a>]'
                % (users.create_login_url(self.req.url()), self.req._('login')))
 
 class GAELogoutAction(LogoutAction):
--- a/hooks/notification.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/hooks/notification.py	Wed Aug 26 14:45:56 2009 +0200
@@ -47,9 +47,10 @@
         if view is None:
             return
         comment = entity.printable_value('comment', format='text/plain')
-        if comment:
-            comment = normalize_text(comment, 80,
-                                     rest=entity.comment_format=='text/rest')
+        # XXX don't try to wrap rest until we've a proper transformation (see
+        # #103822)
+        if comment and entity.comment_format != 'text/rest':
+            comment = normalize_text(comment, 80)
         RenderAndSendNotificationView(self._cw, view=view, viewargs={
             'comment': comment, 'previous_state': entity.previous_state.name,
             'current_state': entity.new_state.name})
--- a/i18n/en.po	Fri Aug 21 16:26:20 2009 +0200
+++ b/i18n/en.po	Wed Aug 26 14:45:56 2009 +0200
@@ -75,31 +75,31 @@
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;days"
+msgid "%d&#160;days"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;hours"
+msgid "%d&#160;hours"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;minutes"
+msgid "%d&#160;minutes"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;months"
+msgid "%d&#160;months"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;seconds"
+msgid "%d&#160;seconds"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;weeks"
+msgid "%d&#160;weeks"
 msgstr ""
 
 #, python-format
-msgid "%d&nbsp;years"
+msgid "%d&#160;years"
 msgstr ""
 
 #, python-format
--- a/i18n/es.po	Fri Aug 21 16:26:20 2009 +0200
+++ b/i18n/es.po	Wed Aug 26 14:45:56 2009 +0200
@@ -80,32 +80,32 @@
 msgstr "%d años"
 
 #, python-format
-msgid "%d&nbsp;days"
-msgstr "%d&nbsp;días"
+msgid "%d&#160;days"
+msgstr "%d&#160;días"
 
 #, python-format
-msgid "%d&nbsp;hours"
-msgstr "%d&nbsp;horas"
+msgid "%d&#160;hours"
+msgstr "%d&#160;horas"
 
 #, python-format
-msgid "%d&nbsp;minutes"
-msgstr "%d&nbsp;minutos"
+msgid "%d&#160;minutes"
+msgstr "%d&#160;minutos"
 
 #, python-format
-msgid "%d&nbsp;months"
-msgstr "%d&nbsp;meses"
+msgid "%d&#160;months"
+msgstr "%d&#160;meses"
 
 #, python-format
-msgid "%d&nbsp;seconds"
-msgstr "%d&nbsp;segundos"
+msgid "%d&#160;seconds"
+msgstr "%d&#160;segundos"
 
 #, python-format
-msgid "%d&nbsp;weeks"
-msgstr "%d&nbsp;semanas"
+msgid "%d&#160;weeks"
+msgstr "%d&#160;semanas"
 
 #, python-format
-msgid "%d&nbsp;years"
-msgstr "%d&nbsp;años"
+msgid "%d&#160;years"
+msgstr "%d&#160;años"
 
 #, python-format
 msgid "%s error report"
--- a/i18n/fr.po	Fri Aug 21 16:26:20 2009 +0200
+++ b/i18n/fr.po	Wed Aug 26 14:45:56 2009 +0200
@@ -80,32 +80,32 @@
 msgstr "%d années"
 
 #, python-format
-msgid "%d&nbsp;days"
-msgstr "%d&nbsp;jours"
+msgid "%d&#160;days"
+msgstr "%d&#160;jours"
 
 #, python-format
-msgid "%d&nbsp;hours"
-msgstr "%d&nbsp;heures"
+msgid "%d&#160;hours"
+msgstr "%d&#160;heures"
 
 #, python-format
-msgid "%d&nbsp;minutes"
-msgstr "%d&nbsp;minutes"
+msgid "%d&#160;minutes"
+msgstr "%d&#160;minutes"
 
 #, python-format
-msgid "%d&nbsp;months"
-msgstr "%d&nbsp;mois"
+msgid "%d&#160;months"
+msgstr "%d&#160;mois"
 
 #, python-format
-msgid "%d&nbsp;seconds"
-msgstr "%d&nbsp;secondes"
+msgid "%d&#160;seconds"
+msgstr "%d&#160;secondes"
 
 #, python-format
-msgid "%d&nbsp;weeks"
-msgstr "%d&nbsp;semaines"
+msgid "%d&#160;weeks"
+msgstr "%d&#160;semaines"
 
 #, python-format
-msgid "%d&nbsp;years"
-msgstr "%d&nbsp;années"
+msgid "%d&#160;years"
+msgstr "%d&#160;années"
 
 #, python-format
 msgid "%s error report"
--- a/misc/migration/postcreate.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/misc/migration/postcreate.py	Wed Aug 26 14:45:56 2009 +0200
@@ -6,14 +6,30 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
-activatedeid = add_state(_('activated'), 'CWUser', initial=True)
-deactivatedeid = add_state(_('deactivated'), 'CWUser')
-add_transition(_('deactivate'), 'CWUser',
-               (activatedeid,), deactivatedeid,
-               requiredgroups=('managers',))
-add_transition(_('activate'), 'CWUser',
-               (deactivatedeid,), activatedeid,
-               requiredgroups=('managers',))
+# insert versions
+create_entity('CWProperty', pkey=u'system.version.cubicweb',
+              value=unicode(config.cubicweb_version()))
+for cube in config.cubes():
+    create_entity('CWProperty', pkey=u'system.version.%s' % cube.lower(),
+                  value=unicode(config.cube_version(cube)))
+
+# some entities have been added before schema entities, fix the 'is' and
+# 'is_instance_of' relations
+for rtype in ('is', 'is_instance_of'):
+    sql('INSERT INTO %s_relation '
+        'SELECT X.eid, ET.cw_eid FROM entities as X, cw_CWEType as ET '
+        'WHERE X.type=ET.cw_name AND NOT EXISTS('
+        '      SELECT 1 from is_relation '
+        '      WHERE eid_from=X.eid AND eid_to=ET.cw_eid)' % rtype)
+
+# user workflow
+userwf = add_workflow(_('default user workflow'), 'CWUser')
+activated = userwf.add_state(_('activated'), initial=True)
+deactivated = userwf.add_state(_('deactivated'))
+userwf.add_transition(_('deactivate'), (activated,), deactivated,
+                      requiredgroups=('managers',))
+userwf.add_transition(_('activate'), (deactivated,), activated,
+                      requiredgroups=('managers',))
 
 # create anonymous user if all-in-one config and anonymous user has been specified
 if hasattr(config, 'anonymous_user'):
@@ -26,23 +42,23 @@
 # need this since we already have at least one user in the database (the default admin)
 for user in rql('Any X WHERE X is CWUser').entities():
     session.unsafe_execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
-                           {'x': user.eid, 's': activatedeid}, 'x')
-
-cfg = config.persistent_options_configuration()
-if interactive_mode:
-    cfg.input_config(inputlevel=0)
+                           {'x': user.eid, 's': activated.eid}, 'x')
 
-for section, options in cfg.options_by_section():
-    for optname, optdict, value in options:
-        key = '%s.%s' % (section, optname)
-        default = cfg.option_default(optname, optdict)
-        # only record values differing from default
-        if value != default:
-            rql('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', {'k': key, 'v': value})
+# on interactive mode, ask for level 0 persistent options
+if interactive_mode:
+    cfg = config.persistent_options_configuration()
+    cfg.input_config(inputlevel=0)
+    for section, options in cfg.options_by_section():
+        for optname, optdict, value in options:
+            key = '%s.%s' % (section, optname)
+            default = cfg.option_default(optname, optdict)
+            # only record values differing from default
+            if value != default:
+                rql('INSERT CWProperty X: X pkey %(k)s, X value %(v)s', {'k': key, 'v': value})
 
 # add PERM_USE_TEMPLATE_FORMAT permission
 from cubicweb.schema import PERM_USE_TEMPLATE_FORMAT
-eid = add_entity('CWPermission', name=PERM_USE_TEMPLATE_FORMAT,
-                 label=_('use template languages'))
+usetmplperm = create_entity('CWPermission', name=PERM_USE_TEMPLATE_FORMAT,
+                            label=_('use template languages'))
 rql('SET X require_group G WHERE G name "managers", X eid %(x)s',
-    {'x': eid}, 'x')
+    {'x': usetmplperm.eid}, 'x')
--- a/rset.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/rset.py	Wed Aug 26 14:45:56 2009 +0200
@@ -447,7 +447,7 @@
             if rqlst.TYPE == 'select':
                 # UNION query, find the subquery from which this entity has been
                 # found
-                rqlst = rqlst.locate_subquery(col, etype, self.args)
+                rqlst, col = rqlst.locate_subquery(col, etype, self.args)
             # take care, due to outer join support, we may find None
             # values for non final relation
             for i, attr, x in attr_desc_iterator(rqlst, col):
@@ -547,7 +547,8 @@
                     if len(self.column_types(i)) > 1:
                         break
         # UNION query, find the subquery from which this entity has been found
-        select = rqlst.locate_subquery(locate_query_col, etype, self.args)
+        select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0]
+        col = rqlst.subquery_selection_index(select, col)
         try:
             myvar = select.selection[col].variable
         except AttributeError:
@@ -555,7 +556,7 @@
             return None, None
         rel = myvar.main_relation()
         if rel is not None:
-            index = rel.children[0].variable.selected_index()
+            index = rel.children[0].root_selection_index()
             if index is not None and self.rows[row][index]:
                 return self.get_entity(row, index), rel.r_type
         return None, None
--- a/schemas/workflow.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/schemas/workflow.py	Wed Aug 26 14:45:56 2009 +0200
@@ -58,7 +58,7 @@
     allowed_transition = SubjectRelation('BaseTransition', cardinality='**',
                                          constraints=[RQLConstraint('S state_of WF, O transition_of WF')],
                                          description=_('allowed transitions from this state'))
-    state_of = SubjectRelation('Workflow', cardinality='+*',
+    state_of = SubjectRelation('Workflow', cardinality='1*',
                                description=_('workflow to which this state belongs'),
                                constraints=[RQLUniqueConstraint('S name N, Y state_of O, Y name N')])
 
@@ -81,7 +81,7 @@
     require_group = SubjectRelation('CWGroup', cardinality='**',
                                     description=_('group in which a user should be to be '
                                                   'allowed to pass this transition'))
-    transition_of = SubjectRelation('Workflow', cardinality='+*',
+    transition_of = SubjectRelation('Workflow', cardinality='1*',
                                     description=_('workflow to which this transition belongs'),
                                     constraints=[RQLUniqueConstraint('S name N, Y transition_of O, Y name N')])
 
--- a/schemaviewer.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/schemaviewer.py	Wed Aug 26 14:45:56 2009 +0200
@@ -104,7 +104,7 @@
         """get a layout for an entity schema"""
         etype = eschema.type
         layout = Section(children=' ', klass='clear')
-        layout.append(Link(etype,'&nbsp;' , id=etype)) # anchor
+        layout.append(Link(etype,'&#160;' , id=etype)) # anchor
         title = Link(self.eschema_link_url(eschema), etype)
         boxchild = [Section(children=(title, ' (%s)'% eschema.display_name(self.req)), klass='title')]
         table = Table(cols=4, rheaders=1,
--- a/server/__init__.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/server/__init__.py	Wed Aug 26 14:45:56 2009 +0200
@@ -180,22 +180,6 @@
     handler = config.migration_handler(schema, interactive=False,
                                        cnx=cnx, repo=repo)
     initialize_schema(config, schema, handler)
-    # insert versions
-    handler.cmd_add_entity('CWProperty', pkey=u'system.version.cubicweb',
-                           value=unicode(config.cubicweb_version()))
-    for cube in config.cubes():
-        handler.cmd_add_entity('CWProperty',
-                               pkey=u'system.version.%s' % cube.lower(),
-                               value=unicode(config.cube_version(cube)))
-    # some entities have been added before schema entities, fix the 'is' and
-    # 'is_instance_of' relations
-    for rtype in ('is', 'is_instance_of'):
-        handler.sqlexec(
-            'INSERT INTO %s_relation '
-            'SELECT X.eid, ET.cw_eid FROM entities as X, cw_CWEType as ET '
-            'WHERE X.type=ET.cw_name AND NOT EXISTS('
-            '      SELECT 1 from is_relation '
-            '      WHERE eid_from=X.eid AND eid_to=ET.cw_eid)' % rtype)
     # yoo !
     cnx.commit()
     config.enabled_sources = None
--- a/server/migractions.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/server/migractions.py	Wed Aug 26 14:45:56 2009 +0200
@@ -1022,7 +1022,7 @@
         if commit:
             self.commit()
 
-    @deprecated('use entity.change_state("state")')
+    @deprecated('use entity.fire_transition("transition") or entity.change_state("state")')
     def cmd_set_state(self, eid, statename, commit=False):
         self.session.set_pool() # ensure pool is set
         self.session.entity_from_eid(eid).change_state(statename)
--- a/server/serverctl.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/server/serverctl.py	Wed Aug 26 14:45:56 2009 +0200
@@ -257,12 +257,19 @@
           'help': 'verbose mode: will ask all possible configuration questions',
           }
          ),
+        ('automatic',
+         {'short': 'a', 'type' : 'yn', 'metavar': '<auto>',
+          'default': 'n',
+          'help': 'automatic mode: never ask and use default answer to every question',
+          }
+         ),
         )
     def run(self, args):
         """run the command with its specific arguments"""
         from logilab.common.adbh import get_adv_func_helper
         from indexer import get_indexer
         verbose = self.get('verbose')
+        automatic = self.get('automatic')
         appid = pop_arg(args, msg='No instance specified !')
         config = ServerConfiguration.config_for(appid)
         create_db = self.config.create_db
@@ -277,13 +284,13 @@
             try:
                 if helper.users_support:
                     user = source['db-user']
-                    if not helper.user_exists(cursor, user) and \
-                           ASK.confirm('Create db user %s ?' % user, default_is_yes=False):
+                    if not helper.user_exists(cursor, user) and (automatic or \
+                           ASK.confirm('Create db user %s ?' % user, default_is_yes=False)):
                         helper.create_user(source['db-user'], source['db-password'])
                         print '-> user %s created.' % user
                 dbname = source['db-name']
                 if dbname in helper.list_databases(cursor):
-                    if ASK.confirm('Database %s already exists -- do you want to drop it ?' % dbname):
+                    if automatic or ASK.confirm('Database %s already exists -- do you want to drop it ?' % dbname):
                         cursor.execute('DROP DATABASE %s' % dbname)
                     else:
                         return
@@ -311,7 +318,7 @@
         cnx.commit()
         print '-> database for instance %s created and necessary extensions installed.' % appid
         print
-        if ASK.confirm('Run db-init to initialize the system database ?'):
+        if automatic or ASK.confirm('Run db-init to initialize the system database ?'):
             cmd_run('db-init', config.appid)
         else:
             print ('-> nevermind, you can do it later with '
--- a/server/sources/pyrorql.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/server/sources/pyrorql.py	Wed Aug 26 14:45:56 2009 +0200
@@ -26,6 +26,12 @@
 from cubicweb.server.sources import (AbstractSource, ConnectionWrapper,
                                      TimedCache, dbg_st_search, dbg_results)
 
+
+def uidtype(union, col, etype, args):
+    select, col = union.locate_subquery(col, etype, args)
+    return getattr(select.selection[col], 'uidtype', None)
+
+
 class ReplaceByInOperator(Exception):
     def __init__(self, eids):
         self.eids = eids
@@ -295,8 +301,8 @@
             needtranslation = []
             rows = rset.rows
             for i, etype in enumerate(descr[0]):
-                if (etype is None or not self.schema.eschema(etype).is_final() or
-                    getattr(union.locate_subquery(i, etype, args).selection[i], 'uidtype', None)):
+                if (etype is None or not self.schema.eschema(etype).is_final()
+                    or uidtype(union, i, etype, args)):
                     needtranslation.append(i)
             if needtranslation:
                 cnx = session.pool.connection(self.uri)
--- a/sobjects/notification.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/sobjects/notification.py	Wed Aug 26 14:45:56 2009 +0200
@@ -104,8 +104,12 @@
         entity = self.rset.get_entity(self.row or 0, self.col or 0)
         content = entity.printable_value(self.content_attr, format='text/plain')
         if content:
-            contentformat = getattr(entity, self.content_attr + '_format', 'text/rest')
-            content = normalize_text(content, 80, rest=contentformat=='text/rest')
+            contentformat = getattr(entity, self.content_attr + '_format',
+                                    'text/rest')
+            # XXX don't try to wrap rest until we've a proper transformation (see
+            # #103822)
+            if contentformat != 'text/rest':
+                content = normalize_text(content, 80)
         return super(ContentAddedView, self).context(content=content, **kwargs)
 
     def subject(self):
--- a/test/unittest_rset.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/test/unittest_rset.py	Wed Aug 26 14:45:56 2009 +0200
@@ -328,6 +328,7 @@
         entity, rtype = rset.related_entity(1, 1)
         self.assertEquals(entity.id, 'CWGroup')
         self.assertEquals(rtype, 'name')
+        #
         rset = self.execute('Any X,N ORDERBY N WHERE X is Bookmark WITH X,N BEING '
                             '((Any X,N WHERE X is CWGroup, X name N)'
                             ' UNION '
@@ -335,6 +336,14 @@
         entity, rtype = rset.related_entity(0, 1)
         self.assertEquals(entity.eid, e.eid)
         self.assertEquals(rtype, 'title')
+        #
+        rset = self.execute('Any X,N ORDERBY N WITH N,X BEING '
+                            '((Any N,X WHERE X is CWGroup, X name N)'
+                            ' UNION '
+                            ' (Any N,X WHERE X is Bookmark, X title N))')
+        entity, rtype = rset.related_entity(0, 1)
+        self.assertEquals(entity.eid, e.eid)
+        self.assertEquals(rtype, 'title')
 
     def test_entities(self):
         rset = self.execute('Any U,G WHERE U in_group G')
--- a/view.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/view.py	Wed Aug 26 14:45:56 2009 +0200
@@ -474,8 +474,11 @@
     __registry__ = 'components'
     __select__ = yes()
 
+    # XXX huummm, much probably useless
+    htmlclass = 'mainRelated'
     def div_class(self):
-        return '%s %s' % (self.cw_propval('htmlclass'), self.id)
+        return '%s %s' % (self.htmlclass, self.id)
 
+    # XXX a generic '%s%s' % (self.id, self.__registry__.capitalize()) would probably be nicer
     def div_id(self):
         return '%sComponent' % self.id
--- a/web/component.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/component.py	Wed Aug 26 14:45:56 2009 +0200
@@ -8,6 +8,8 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
+from simplejson import dumps
+
 from logilab.common.deprecation import class_renamed
 from logilab.mtconverter import xml_escape
 
@@ -43,8 +45,6 @@
                                         _('navcontenttop'), _('navcontentbottom')),
                             #vocabulary=(_('header'), _('incontext'), _('footer')),
                             help=_('context where this component should be displayed')),
-        _('htmlclass'):dict(type='String', default='mainRelated',
-                            help=_('html class of the component')),
     }
 
     context = 'navcontentbottom' # 'footer' | 'header' | 'incontext'
@@ -72,7 +72,8 @@
     page_link_templ = u'<span class="slice"><a href="%s" title="%s">%s</a></span>'
     selected_page_link_templ = u'<span class="selectedSlice"><a href="%s" title="%s">%s</a></span>'
     previous_page_link_templ = next_page_link_templ = page_link_templ
-    no_previous_page_link = no_next_page_link = u''
+    no_previous_page_link = u'&lt;&lt;'
+    no_next_page_link = u'&gt;&gt;'
 
     def __init__(self, req, rset, **kwargs):
         super(NavigationComponent, self).__init__(req, rset=rset, **kwargs)
@@ -112,33 +113,40 @@
         if self.stop_param in params:
             del params[self.stop_param]
 
+    def page_url(self, path, params, start, stop):
+        params = merge_dicts(params, {self.start_param : start,
+                                      self.stop_param : stop,})
+        if path == 'json':
+            rql = params.pop('rql', self.rset.printable_rql())
+            # latest 'true' used for 'swap' mode
+            url = 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % (
+                dumps(params.get('divid', 'paginated-content')),
+                dumps(rql), dumps(params.pop('vid', None)), dumps(params))
+        else:
+            url = self.build_url(path, **params)
+        return url
+
     def page_link(self, path, params, start, stop, content):
-        url = self.build_url(path, **merge_dicts(params, {self.start_param : start,
-                                                          self.stop_param : stop,}))
-        url = xml_escape(url)
+        url = xml_escape(self.page_url(path, params, start, stop))
         if start == self.starting_from:
             return self.selected_page_link_templ % (url, content, content)
         return self.page_link_templ % (url, content, content)
 
-    def previous_link(self, params, content='&lt;&lt;', title=_('previous_results')):
+    def previous_link(self, path, params, content='&lt;&lt;', title=_('previous_results')):
         start = self.starting_from
         if not start :
             return self.no_previous_page_link
         start = max(0, start - self.page_size)
         stop = start + self.page_size - 1
-        url = self.build_url(**merge_dicts(params, {self.start_param : start,
-                                                    self.stop_param : stop,}))
-        url = xml_escape(url)
+        url = xml_escape(self.page_url(path, params, start, stop))
         return self.previous_page_link_templ % (url, title, content)
 
-    def next_link(self, params, content='&gt;&gt;', title=_('next_results')):
+    def next_link(self, path, params, content='&gt;&gt;', title=_('next_results')):
         start = self.starting_from + self.page_size
         if start >= self.total:
             return self.no_next_page_link
         stop = start + self.page_size - 1
-        url = self.build_url(**merge_dicts(params, {self.start_param : start,
-                                                    self.stop_param : stop,}))
-        url = xml_escape(url)
+        url = xml_escape(self.page_url(path, params, start, stop))
         return self.next_page_link_templ % (url, title, content)
 
 
--- a/web/data/cubicweb.ajax.js	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/data/cubicweb.ajax.js	Wed Aug 26 14:45:56 2009 +0200
@@ -66,7 +66,16 @@
     jQuery(CubicWeb).trigger('ajax-loaded');
 }
 
-// cubicweb loadxhtml plugin to make jquery handle xhtml response
+/* cubicweb loadxhtml plugin to make jquery handle xhtml response
+ *
+ * fetches `url` and replaces this's content with the result
+ *
+ * @param mode how the replacement should be done (default is 'replace')
+ *  Possible values are :
+ *    - 'replace' to replace the node's content with the generated HTML
+ *    - 'swap' to replace the node itself with the generated HTML
+ *    - 'append' to append the generated HTML to the node's content
+ */
 jQuery.fn.loadxhtml = function(url, data, reqtype, mode) {
     var ajax = null;
     if (reqtype == 'post') {
@@ -323,7 +332,7 @@
     }
 }
 
-/*
+/* XXX deprecates?
  * fetches `url` and replaces `nodeid`'s content with the result
  * @param replacemode how the replacement should be done (default is 'replace')
  *  Possible values are :
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.facets.js	Wed Aug 26 14:45:56 2009 +0200
@@ -0,0 +1,227 @@
+/*
+ *  :organization: Logilab
+ *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ */
+
+CubicWeb.require('htmlhelpers.js');
+CubicWeb.require('ajax.js');
+
+//============= filter form functions ========================================//
+function copyParam(origparams, newparams, param) {
+    var index = findValue(origparams[0], param);
+    if (index > -1) {
+	newparams[param] = origparams[1][index];
+    }
+}
+
+function facetFormContent(form) {
+    var names = [];
+    var values = [];
+    jQuery(form).find('.facet').each(function () {
+        var facetName = jQuery(this).find('.facetTitle').attr('cubicweb:facetName');
+        var facetValues = jQuery(this).find('.facetValueSelected').each(function(x) {
+  	    names.push(facetName);
+  	    values.push(this.getAttribute('cubicweb:value'));
+        });
+    });
+    jQuery(form).find('input').each(function () {
+        names.push(this.name);
+        values.push(this.value);
+    });
+    jQuery(form).find('select option[selected]').each(function () {
+	names.push(this.parentNode.name);
+	values.push(this.value);
+    });
+    return [names, values];
+}
+
+function buildRQL(divid, vid, paginate, vidargs) {
+    jQuery(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]);
+    var form = getNode(divid+'Form');
+    var zipped = facetFormContent(form);
+    zipped[0].push('facetargs');
+    zipped[1].push(vidargs);
+    var d = asyncRemoteExec('filter_build_rql', zipped[0], zipped[1]);
+    d.addCallback(function(result) {
+	var rql = result[0];
+	var $bkLink = jQuery('#facetBkLink');
+	if ($bkLink.length) {
+	    var bkUrl = $bkLink.attr('cubicweb:target') + '&path=view?rql=' + rql;
+	    if (vid) {
+		bkUrl += '&vid=' + vid;
+	    }
+	    $bkLink.attr('href', bkUrl);
+	}
+	var toupdate = result[1];
+	var extraparams = vidargs;
+	var displayactions = jQuery('#' + divid).attr('cubicweb:displayactions');
+	if (displayactions) { extraparams['displayactions'] = displayactions; }
+	if (paginate) { extraparams['paginate'] = '1'; }
+	// copy some parameters
+	// XXX cleanup vid/divid mess
+	// if vid argument is specified , the one specified in form params will
+	// be overriden by replacePageChunk
+	copyParam(zipped, extraparams, 'vid');
+	extraparams['divid'] = divid;
+	copyParam(zipped, extraparams, 'divid');
+	copyParam(zipped, extraparams, 'subvid');
+	// paginate used to know if the filter box is acting, in which case we
+	// want to reload action box to match current selection (we don't want
+	// this from a table filter)
+	replacePageChunk(divid, rql, vid, extraparams, true, function() {
+	  jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
+	});
+	if (paginate) {
+	    // FIXME the edit box might not be displayed in which case we don't
+	    // know where to put the potential new one, just skip this case
+	    // for now
+	    if (jQuery('#edit_box').length) {
+		reloadComponent('edit_box', rql, 'boxes', 'edit_box');
+	    }
+	    if (jQuery('#breadcrumbs').length) {
+		reloadComponent('breadcrumbs', rql, 'components', 'breadcrumbs');
+	    }
+	}
+	var d = asyncRemoteExec('filter_select_content', toupdate, rql);
+	d.addCallback(function(updateMap) {
+	    for (facetId in updateMap) {
+		var values = updateMap[facetId];
+		jqNode(facetId).find('.facetCheckBox').each(function () {
+		    var value = this.getAttribute('cubicweb:value');
+		    if (!values.contains(value)) {
+			if (!jQuery(this).hasClass('facetValueDisabled')) {
+			    jQuery(this).addClass('facetValueDisabled');
+			}
+		    } else {
+			if (jQuery(this).hasClass('facetValueDisabled')) {
+			    jQuery(this).removeClass('facetValueDisabled');
+			}
+		    }
+		});
+	    }
+	});
+    });
+}
+
+
+var SELECTED_IMG = baseuri()+"data/black-check.png";
+var UNSELECTED_IMG = baseuri()+"data/no-check-no-border.png";
+var UNSELECTED_BORDER_IMG = baseuri()+"data/black-uncheck.png";
+
+function initFacetBoxEvents(root) {
+    // facetargs : (divid, vid, paginate, extraargs)
+    root = root || document;
+    jQuery(root).find('form').each(function () {
+	var form = jQuery(this);
+	// NOTE: don't evaluate facetargs here but in callbacks since its value
+	//       may changes and we must send its value when the callback is
+	//       called, not when the page is initialized
+	var facetargs = form.attr('cubicweb:facetargs');
+	if (facetargs !== undefined) {
+	    form.submit(function() {
+	        buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
+	        return false;
+	    });
+	    form.find('div.facet').each(function() {
+		var facet = jQuery(this);
+		facet.find('div.facetCheckBox').each(function (i) {
+		    this.setAttribute('cubicweb:idx', i);
+		});
+		facet.find('div.facetCheckBox').click(function () {
+		    var $this = jQuery(this);
+		    if ($this.hasClass('facetValueDisabled')){
+		     	    return
+		    }
+		    if ($this.hasClass('facetValueSelected')) {
+			$this.removeClass('facetValueSelected');
+			$this.find('img').each(function (i){
+			if (this.getAttribute('cubicweb:unselimg')){
+			       this.setAttribute('src', UNSELECTED_BORDER_IMG);
+			       this.setAttribute('alt', (_('not selected')));
+			    }
+			    else{
+			       this.setAttribute('src', UNSELECTED_IMG);
+			       this.setAttribute('alt', (_('not selected')));
+			    }
+			});
+			var index = parseInt($this.attr('cubicweb:idx'));
+			// we dont need to move the element when cubicweb:idx == 0
+			if (index > 0){
+			    var shift = jQuery.grep(facet.find('.facetValueSelected'), function (n) {
+				    var nindex = parseInt(n.getAttribute('cubicweb:idx'));
+				    return nindex > index;
+				}).length;
+			    index += shift;
+			    var parent = this.parentNode;
+			    var $insertAfter = jQuery(parent).find('.facetCheckBox:nth('+index+')');
+			    if ( ! ($insertAfter.length == 1 && shift == 0) ) {
+				// only rearrange element if necessary
+				$insertAfter.after(this);
+			    }
+			}
+		    } else {
+			var lastSelected = facet.find('.facetValueSelected:last');
+			if (lastSelected.length) {
+			    lastSelected.after(this);
+			} else {
+			    var parent = this.parentNode;
+			    jQuery(parent).prepend(this);
+			}
+			jQuery(this).addClass('facetValueSelected');
+			var $img = jQuery(this).find('img');
+			$img.attr('src', SELECTED_IMG).attr('alt', (_('selected')));
+		    }
+		    buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
+		    facet.find('.facetBody').animate({scrollTop: 0}, '');
+		});
+		facet.find('select.facetOperator').change(function() {
+		    var nbselected = facet.find('div.facetValueSelected').length;
+		    if (nbselected >= 2) {
+			buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
+		    }
+		});
+		facet.find('div.facetTitle').click(function() {
+		  facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
+		  jQuery(this).toggleClass('opened');
+		   });
+
+	    });
+	}
+    });
+}
+
+// trigger this function on document ready event if you provide some kind of
+// persistent search (eg crih)
+function reorderFacetsItems(root){
+    root = root || document;
+    jQuery(root).find('form').each(function () {
+	var form = jQuery(this);
+	if (form.attr('cubicweb:facetargs')) {
+	    form.find('div.facet').each(function() {
+		var facet = jQuery(this);
+		var lastSelected = null;
+		facet.find('div.facetCheckBox').each(function (i) {
+		    var $this = jQuery(this);
+		    if ($this.hasClass('facetValueSelected')) {
+			if (lastSelected) {
+			    lastSelected.after(this);
+			} else {
+			    var parent = this.parentNode;
+			    jQuery(parent).prepend(this);
+			}
+			lastSelected = $this;
+		    }
+		});
+	    });
+	}
+    });
+}
+
+// we need to differenciate cases where initFacetBoxEvents is called
+// with one argument or without any argument. If we use `initFacetBoxEvents`
+// as the direct callback on the jQuery.ready event, jQuery will pass some argument
+// of his, so we use this small anonymous function instead.
+jQuery(document).ready(function() {initFacetBoxEvents();});
+
+CubicWeb.provide('facets.js');
--- a/web/data/cubicweb.form.css	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/data/cubicweb.form.css	Wed Aug 26 14:45:56 2009 +0200
@@ -78,7 +78,7 @@
 
 table.attributeForm th,
 table.attributeForm td {
-  padding : .7em 2px;
+  padding : .2em 2px;
 }
 
 table.attributeForm th {
--- a/web/data/cubicweb.formfilter.js	Fri Aug 21 16:26:20 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,223 +0,0 @@
-/*
- *  :organization: Logilab
- *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
- *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
- */
-
-CubicWeb.require('htmlhelpers.js');
-CubicWeb.require('ajax.js');
-
-//============= filter form functions ========================================//
-function copyParam(origparams, newparams, param) {
-    var index = findValue(origparams[0], param);
-    if (index > -1) {
-	newparams[param] = origparams[1][index];
-    }
-}
-
-function facetFormContent(form) {
-    var names = [];
-    var values = [];
-    jQuery(form).find('.facet').each(function () {
-        var facetName = jQuery(this).find('.facetTitle').attr('cubicweb:facetName');
-        var facetValues = jQuery(this).find('.facetValueSelected').each(function(x) {
-  	    names.push(facetName);
-  	    values.push(this.getAttribute('cubicweb:value'));
-        });
-    });
-    jQuery(form).find('input').each(function () {
-        names.push(this.name);
-        values.push(this.value);
-    });
-    jQuery(form).find('select option[selected]').each(function () {
-	names.push(this.parentNode.name);
-	values.push(this.value);
-    });
-    return [names, values];
-}
-
-function buildRQL(divid, vid, paginate, vidargs) {
-    jQuery(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]);
-    var form = getNode(divid+'Form');
-    var zipped = facetFormContent(form);
-    zipped[0].push('facetargs');
-    zipped[1].push(vidargs);
-    var d = asyncRemoteExec('filter_build_rql', zipped[0], zipped[1]);
-    d.addCallback(function(result) {
-	var rql = result[0];
-	var $bkLink = jQuery('#facetBkLink');
-	if ($bkLink.length) {
-	    var bkUrl = $bkLink.attr('cubicweb:target') + '&path=view?rql=' + rql;
-	    if (vid) {
-		bkUrl += '&vid=' + vid;
-	    }
-	    $bkLink.attr('href', bkUrl);
-	}
-	var toupdate = result[1];
-	var extraparams = vidargs;
-	var displayactions = jQuery('#' + divid).attr('cubicweb:displayactions');
-	if (displayactions) { extraparams['displayactions'] = displayactions; }
-	if (paginate) { extraparams['paginate'] = '1'; }
-	// copy some parameters
-	// XXX cleanup vid/divid mess
-	// if vid argument is specified , the one specified in form params will
-	// be overriden by replacePageChunk
-	copyParam(zipped, extraparams, 'vid');
-	extraparams['divid'] = divid;
-	copyParam(zipped, extraparams, 'divid');
-	copyParam(zipped, extraparams, 'subvid');
-	// paginate used to know if the filter box is acting, in which case we
-	// want to reload action box to match current selection
-	replacePageChunk(divid, rql, vid, extraparams, true, function() {
-	  jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
-	});
-	if (paginate) {
-	    // FIXME the edit box might not be displayed in which case we don't
-	    // know where to put the potential new one, just skip this case
-	    // for now
-	    if (jQuery('#edit_box').length) {
-		reloadComponent('edit_box', rql, 'boxes', 'edit_box');
-	    }
-	}
-	var d = asyncRemoteExec('filter_select_content', toupdate, rql);
-	d.addCallback(function(updateMap) {
-	    for (facetId in updateMap) {
-		var values = updateMap[facetId];
-		jqNode(facetId).find('.facetCheckBox').each(function () {
-		    var value = this.getAttribute('cubicweb:value');
-		    if (!values.contains(value)) {
-			if (!jQuery(this).hasClass('facetValueDisabled')) {
-			    jQuery(this).addClass('facetValueDisabled');
-			}
-		    } else {
-			if (jQuery(this).hasClass('facetValueDisabled')) {
-			    jQuery(this).removeClass('facetValueDisabled');
-			}
-		    }
-		});
-	    }
-	});
-    });
-}
-
-
-var SELECTED_IMG = baseuri()+"data/black-check.png";
-var UNSELECTED_IMG = baseuri()+"data/no-check-no-border.png";
-var UNSELECTED_BORDER_IMG = baseuri()+"data/black-uncheck.png";
-
-function initFacetBoxEvents(root) {
-    // facetargs : (divid, vid, paginate, extraargs)
-    root = root || document;
-    jQuery(root).find('form').each(function () {
-	var form = jQuery(this);
-	// NOTE: don't evaluate facetargs here but in callbacks since its value
-	//       may changes and we must send its value when the callback is
-	//       called, not when the page is initialized
-	var facetargs = form.attr('cubicweb:facetargs');
-	if (facetargs !== undefined) {
-	    form.submit(function() {
-	        buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-	        return false;
-	    });
-	    form.find('div.facet').each(function() {
-		var facet = jQuery(this);
-		facet.find('div.facetCheckBox').each(function (i) {
-		    this.setAttribute('cubicweb:idx', i);
-		});
-		facet.find('div.facetCheckBox').click(function () {
-		    var $this = jQuery(this);
-		    if ($this.hasClass('facetValueDisabled')){
-		     	    return
-		    }
-		    if ($this.hasClass('facetValueSelected')) {
-			$this.removeClass('facetValueSelected');
-			$this.find('img').each(function (i){
-			if (this.getAttribute('cubicweb:unselimg')){
-			       this.setAttribute('src', UNSELECTED_BORDER_IMG);
-			       this.setAttribute('alt', (_('not selected')));
-			    }
-			    else{
-			       this.setAttribute('src', UNSELECTED_IMG);
-			       this.setAttribute('alt', (_('not selected')));
-			    }
-			});
-			var index = parseInt($this.attr('cubicweb:idx'));
-			// we dont need to move the element when cubicweb:idx == 0
-			if (index > 0){
-			    var shift = jQuery.grep(facet.find('.facetValueSelected'), function (n) {
-				    var nindex = parseInt(n.getAttribute('cubicweb:idx'));
-				    return nindex > index;
-				}).length;
-			    index += shift;
-			    var parent = this.parentNode;
-			    var $insertAfter = jQuery(parent).find('.facetCheckBox:nth('+index+')');
-			    if ( ! ($insertAfter.length == 1 && shift == 0) ) {
-				// only rearrange element if necessary
-				$insertAfter.after(this);
-			    }
-			}
-		    } else {
-			var lastSelected = facet.find('.facetValueSelected:last');
-			if (lastSelected.length) {
-			    lastSelected.after(this);
-			} else {
-			    var parent = this.parentNode;
-			    jQuery(parent).prepend(this);
-			}
-			jQuery(this).addClass('facetValueSelected');
-			var $img = jQuery(this).find('img');
-			$img.attr('src', SELECTED_IMG).attr('alt', (_('selected')));
-		    }
-		    buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-		    facet.find('.facetBody').animate({scrollTop: 0}, '');
-		});
-		facet.find('select.facetOperator').change(function() {
-		    var nbselected = facet.find('div.facetValueSelected').length;
-		    if (nbselected >= 2) {
-			buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-		    }
-		});
-		facet.find('div.facetTitle').click(function() {
-		  facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
-		  jQuery(this).toggleClass('opened');
-		   });
-
-	    });
-	}
-    });
-}
-
-// trigger this function on document ready event if you provide some kind of
-// persistent search (eg crih)
-function reorderFacetsItems(root){
-    root = root || document;
-    jQuery(root).find('form').each(function () {
-	var form = jQuery(this);
-	if (form.attr('cubicweb:facetargs')) {
-	    form.find('div.facet').each(function() {
-		var facet = jQuery(this);
-		var lastSelected = null;
-		facet.find('div.facetCheckBox').each(function (i) {
-		    var $this = jQuery(this);
-		    if ($this.hasClass('facetValueSelected')) {
-			if (lastSelected) {
-			    lastSelected.after(this);
-			} else {
-			    var parent = this.parentNode;
-			    jQuery(parent).prepend(this);
-			}
-			lastSelected = $this;
-		    }
-		});
-	    });
-	}
-    });
-}
-
-// we need to differenciate cases where initFacetBoxEvents is called
-// with one argument or without any argument. If we use `initFacetBoxEvents`
-// as the direct callback on the jQuery.ready event, jQuery will pass some argument
-// of his, so we use this small anonymous function instead.
-jQuery(document).ready(function() {initFacetBoxEvents();});
-
-CubicWeb.provide('formfilter.js');
--- a/web/facet.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/facet.py	Wed Aug 26 14:45:56 2009 +0200
@@ -718,7 +718,7 @@
             imgalt = self.req._('not selected')
         self.w(u'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n'
                % (cssclass, xml_escape(unicode(self.value))))
-        self.w(u'<img src="%s" alt="%s"/>&nbsp;' % (imgsrc, imgalt))
+        self.w(u'<img src="%s" alt="%s"/>&#160;' % (imgsrc, imgalt))
         self.w(u'<a href="javascript: {}">%s</a>' % xml_escape(self.label))
         self.w(u'</div>')
 
@@ -747,7 +747,7 @@
         self.w(u'<div class="facetValue facetCheckBox%s" cubicweb:value="%s">\n'
                % (cssclass, xml_escape(unicode(self.value))))
         self.w(u'<div class="facetCheckBoxWidget">')
-        self.w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" />&nbsp;' % (imgsrc, imgalt))
+        self.w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" />&#160;' % (imgsrc, imgalt))
         self.w(u'<label class="facetTitle" cubicweb:facetName="%s"><a href="javascript: {}">%s</a></label>' % (facetid, title))
         self.w(u'</div>\n')
         self.w(u'</div>\n')
@@ -755,7 +755,7 @@
 
 class FacetSeparator(HTMLWidget):
     def __init__(self, label=None):
-        self.label = label or u'&nbsp;'
+        self.label = label or u'&#160;'
 
     def _render(self):
         pass
--- a/web/form.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/form.py	Wed Aug 26 14:45:56 2009 +0200
@@ -162,7 +162,7 @@
                 if len(errors) > 1:
                     templstr = '<li>%s</li>\n'
                 else:
-                    templstr = '&nbsp;%s\n'
+                    templstr = '&#160;%s\n'
                 for field, err in errors:
                     if field is None:
                         errormsg += templstr % err
--- a/web/formwidgets.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/formwidgets.py	Wed Aug 26 14:45:56 2009 +0200
@@ -110,7 +110,7 @@
                   '<br/>',
                   tags.input(name=confirmname, value=values[0], type=self.type,
                              **attrs),
-                  '&nbsp;', tags.span(form.req._('confirm password'),
+                  '&#160;', tags.span(form.req._('confirm password'),
                                       **{'class': 'emphasis'})]
         return u'\n'.join(inputs)
 
@@ -437,7 +437,7 @@
         return super(AddComboBoxWidget, self).render(form, field, renderer) + u'''
 <div id="newvalue">
   <input type="text" id="newopt" />
-  <a href="javascript:noop()" id="add_newopt">&nbsp;</a></div>
+  <a href="javascript:noop()" id="add_newopt">&#160;</a></div>
 '''
 
 # buttons ######################################################################
--- a/web/htmlwidgets.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/htmlwidgets.py	Wed Aug 26 14:45:56 2009 +0200
@@ -75,7 +75,7 @@
             self.w(u'</ul>\n')
         self.w(u'</div>\n')
         if self.shadow:
-            self.w(u'<div class="shadow">&nbsp;</div>')
+            self.w(u'<div class="shadow">&#160;</div>')
 
     def _render(self):
         if self.id:
@@ -191,7 +191,7 @@
         self.value = value
 
     def _render(self):
-        self.w(u'<li><div><span class="label">%s</span>&nbsp;'
+        self.w(u'<li><div><span class="label">%s</span>&#160;'
                u'<span class="value">%s</span></div></li>'
                % (self.label, self.value))
 
--- a/web/test/unittest_form.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/test/unittest_form.py	Wed Aug 26 14:45:56 2009 +0200
@@ -219,7 +219,7 @@
                               '''<input id="upassword:%(eid)s" name="upassword:%(eid)s" tabindex="1" type="password" value="__cubicweb_internal_field__" />
 <br/>
 <input name="upassword-confirm:%(eid)s" tabindex="1" type="password" value="__cubicweb_internal_field__" />
-&nbsp;
+&#160;
 <span class="emphasis">confirm password</span>''' % {'eid': self.entity.eid})
 
 
--- a/web/views/basecomponents.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/basecomponents.py	Wed Aug 26 14:45:56 2009 +0200
@@ -74,7 +74,7 @@
     id = 'help'
     cw_property_defs = VISIBLE_PROP_DEF
     def call(self):
-        self.w(u'<a href="%s" class="help" title="%s">&nbsp;</a>'
+        self.w(u'<a href="%s" class="help" title="%s">&#160;</a>'
                % (self.build_url(_restpath='doc/main'),
                   self.req._(u'help'),))
 
@@ -110,11 +110,11 @@
     def anon_user_link(self):
         if self.config['auth-mode'] == 'cookie':
             self.w(self.req._('anonymous'))
-            self.w(u'''&nbsp;[<a class="logout" href="javascript: popupLoginBox();">%s</a>]'''
+            self.w(u'''&#160;[<a class="logout" href="javascript: popupLoginBox();">%s</a>]'''
                    % (self.req._('i18n_login_popup')))
         else:
             self.w(self.req._('anonymous'))
-            self.w(u'&nbsp;[<a class="logout" href="%s">%s</a>]'
+            self.w(u'&#160;[<a class="logout" href="%s">%s</a>]'
                    % (self.build_url('login'), self.req._('login')))
 
 
@@ -212,7 +212,7 @@
                     url, _('Any')))
         else:
             html.insert(0, u'<span class="selected">%s</span>' % _('Any'))
-        self.w(u'&nbsp;|&nbsp;'.join(html))
+        self.w(u'&#160;|&#160;'.join(html))
         self.w(u'</div>')
 
 class PdfViewComponent(component.Component):
--- a/web/views/basetemplates.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/basetemplates.py	Wed Aug 26 14:45:56 2009 +0200
@@ -429,7 +429,7 @@
                                             req._(ChangeLogView.title).lower()))
         self.w(u'<a href="%s">%s</a> | ' % (req.build_url('doc/about'),
                                             req._('about this site')))
-        self.w(u'© 2001-2009 <a href="http://www.logilab.fr">Logilab S.A.</a>')
+        self.w(u'<a href="http://www.cubicweb.org">%s</a>' % req._('powered by CubicWeb'))
         self.w(u'</div>')
 
 
@@ -478,7 +478,7 @@
         self.w(u'<div id="%s" class="%s">' % (id, klass))
         if title:
             self.w(u'<div id="loginTitle">%s</div>'
-                   % (self.req.property_value('ui.site-title') or u'&nbsp;'))
+                   % (self.req.property_value('ui.site-title') or u'&#160;'))
         self.w(u'<div id="loginContent">\n')
 
         if message:
@@ -509,7 +509,7 @@
         self.w(u'<td><label for="__password" >%s</label></td>' % _('password'))
         self.w(u'<td><input name="__password" id="__password" class="data" type="password" /></td>\n')
         self.w(u'</tr><tr>\n')
-        self.w(u'<td>&nbsp;</td><td><input type="submit" class="loginButton right" value="%s" />\n</td>' % _('log in'))
+        self.w(u'<td>&#160;</td><td><input type="submit" class="loginButton right" value="%s" />\n</td>' % _('log in'))
         self.w(u'</tr>\n')
         self.w(u'</table>\n')
         self.w(u'</form>\n')
--- a/web/views/baseviews.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/baseviews.py	Wed Aug 26 14:45:56 2009 +0200
@@ -51,13 +51,13 @@
     """
     id = 'final'
     # record generated i18n catalog messages
-    _('%d&nbsp;years')
-    _('%d&nbsp;months')
-    _('%d&nbsp;weeks')
-    _('%d&nbsp;days')
-    _('%d&nbsp;hours')
-    _('%d&nbsp;minutes')
-    _('%d&nbsp;seconds')
+    _('%d&#160;years')
+    _('%d&#160;months')
+    _('%d&#160;weeks')
+    _('%d&#160;days')
+    _('%d&#160;hours')
+    _('%d&#160;minutes')
+    _('%d&#160;seconds')
     _('%d years')
     _('%d months')
     _('%d weeks')
@@ -80,7 +80,7 @@
             # value is DateTimeDelta but we have no idea about what is the
             # reference date here, so we can only approximate years and months
             if format == 'text/html':
-                space = '&nbsp;'
+                space = '&#160;'
             else:
                 space = ' '
             if value.days > 730: # 2 years
@@ -111,7 +111,7 @@
         secondary = icon + view(oneline)
         """
         entity = self.rset.get_entity(row, col)
-        self.w(u'&nbsp;')
+        self.w(u'&#160;')
         self.wview('oneline', self.rset, row=row, col=col)
 
 
--- a/web/views/bookmark.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/bookmark.py	Wed Aug 26 14:45:56 2009 +0200
@@ -33,7 +33,7 @@
     def cell_call(self, row, col):
         """the primary view for bookmark entity"""
         entity = self.complete_entity(row, col)
-        self.w(u'&nbsp;')
+        self.w(u'&#160;')
         self.w(u"<span class='title'><b>")
         self.w(u"%s : %s" % (self.req._('Bookmark'), xml_escape(entity.title)))
         self.w(u"</b></span>")
--- a/web/views/boxes.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/boxes.py	Wed Aug 26 14:45:56 2009 +0200
@@ -65,17 +65,8 @@
                not self.schema[self.rset.description[0][0]].is_final() and \
                searchstate == 'normal':
             entity = self.rset.get_entity(0, 0)
-            #entity.complete()
-            if add_menu.items:
-                self.info('explicit actions defined, ignoring potential rtags for %s',
-                          entity.e_schema)
-            else:
-                # some addrelated actions may be specified but no one is selectable
-                # in which case we should not fallback to schema_actions. The proper
-                # way to avoid this is to override add_related_schemas() on the
-                # entity class to return an empty list
-                for action in self.schema_actions(entity):
-                    add_menu.append(action)
+            for action in self.schema_actions(entity):
+                add_menu.append(action)
             self.workflow_actions(entity, box)
         if box.is_empty() and not other_menu.is_empty():
             box.items = other_menu.items
@@ -113,9 +104,9 @@
         """this is actually used ui method to generate 'addrelated' actions from
         the schema.
 
-        If you're using explicit 'addrelated' actions for an entity types, you
-        should probably overrides this method to return an empty list else you
-        may get some unexpected actions.
+        If you don't want any auto-generated actions, you should overrides this
+        method to return an empty list. If you only want some, you can configure
+        them by using uicfg.actionbox_appearsin_addmenu
         """
         req = self.req
         eschema = entity.e_schema
--- a/web/views/calendar.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/calendar.py	Wed Aug 26 14:45:56 2009 +0200
@@ -293,7 +293,7 @@
                                  __redirectvid=self.id
                                  )
             self.w(u'<div class="cmd"><a href="%s">%s</a></div>' % (xml_escape(url), self.req._(u'add')))
-            self.w(u'&nbsp;')
+            self.w(u'&#160;')
         self.w(u'</div>')
         self.w(u'<div class="cellContent">')
         for task_descr in rows:
@@ -312,7 +312,7 @@
                 self.w(u'</div>')
             else:
                 self.w(u'<div class="task">')
-                self.w(u"&nbsp;")
+                self.w(u"&#160;")
             self.w(u'</div>')
         self.w(u'</div>')
         self.w(u'</td>')
@@ -443,7 +443,7 @@
         self.w(u'</tr>')
         self.w(u'</table></div>')
         self.w(u'<div id="coord"></div>')
-        self.w(u'<div id="debug">&nbsp;</div>')
+        self.w(u'<div id="debug">&#160;</div>')
 
     def _build_calendar_cell(self, date, task_descrs):
         inday_tasks = [t for t in task_descrs if t.is_one_day_task() and  t.in_working_hours()]
--- a/web/views/cwuser.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/cwuser.py	Wed Aug 26 14:45:56 2009 +0200
@@ -11,9 +11,10 @@
 
 from cubicweb.selectors import one_line_rset, implements, match_user_groups
 from cubicweb.view import EntityView
-from cubicweb.web import action
+from cubicweb.web import action, uicfg
 from cubicweb.web.views import primary
 
+uicfg.primaryview_section.tag_attribute(('CWUser', 'login'), 'hidden')
 
 class UserPreferencesEntityAction(action.Action):
     id = 'prefs'
--- a/web/views/editcontroller.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/editcontroller.py	Wed Aug 26 14:45:56 2009 +0200
@@ -157,6 +157,7 @@
         errorurl = self.req.form.get('__errorurl')
         if errorurl:
             self.req.cancel_edition(errorurl)
+        self.req.message = self.req._('edit canceled')
         return self.reset()
 
     def _action_delete(self):
--- a/web/views/editviews.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/editviews.py	Wed Aug 26 14:45:56 2009 +0200
@@ -63,7 +63,7 @@
         entity = self.rset.get_entity(row, col)
         erset = entity.as_rset()
         if self.req.match_search_state(erset):
-            self.w(u'<a href="%s" title="%s">%s</a>&nbsp;<a href="%s" title="%s">[...]</a>' % (
+            self.w(u'<a href="%s" title="%s">%s</a>&#160;<a href="%s" title="%s">[...]</a>' % (
                 xml_escape(linksearch_select_url(self.req, erset)),
                 self.req._('select this entity'),
                 xml_escape(entity.view('textoutofcontext')),
--- a/web/views/emailaddress.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/emailaddress.py	Wed Aug 26 14:45:56 2009 +0200
@@ -27,7 +27,7 @@
         if not entity.canonical:
             canonemailaddr = entity.canonical_form()
             if canonemailaddr:
-                self.w(u'&nbsp;(<i>%s</i>)' % canonemailaddr.view('oneline'))
+                self.w(u'&#160;(<i>%s</i>)' % canonemailaddr.view('oneline'))
             self.w(u'</h3>')
         elif entity.identical_to:
             self.w(u'</h3>')
--- a/web/views/facets.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/facets.py	Wed Aug 26 14:45:56 2009 +0200
@@ -40,7 +40,7 @@
     roundcorners = True
 
     needs_css = 'cubicweb.facets.css'
-    needs_js = ('cubicweb.ajax.js', 'cubicweb.formfilter.js')
+    needs_js = ('cubicweb.ajax.js', 'cubicweb.facets.js')
 
     bk_linkbox_template = u'<div class="facetTitle">%s</div>'
 
--- a/web/views/formrenderers.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/formrenderers.py	Wed Aug 26 14:45:56 2009 +0200
@@ -109,7 +109,7 @@
         if example:
             help.append('<div class="helper">(%s: %s)</div>'
                         % (self.req._('sample format'), example))
-        return u'&nbsp;'.join(help)
+        return u'&#160;'.join(help)
 
     # specific methods (mostly to ease overriding) #############################
 
@@ -130,7 +130,7 @@
                 if len(errors) > 1:
                     templstr = '<li>%s</li>\n'
                 else:
-                    templstr = '&nbsp;%s\n'
+                    templstr = '&#160;%s\n'
                 for field, err in errors:
                     if field is None:
                         errormsg += templstr % err
@@ -284,7 +284,7 @@
             if self.display_help:
                 w(self.render_help(form, field))
         # empty slot for buttons
-        w(u'<th class="labelCol">&nbsp;</th>')
+        w(u'<th class="labelCol">&#160;</th>')
         w(u'</tr>')
         w(u'<tr>')
         for field in fields:
@@ -441,7 +441,7 @@
                 w(u'</tr>')
         pendings = list(form.restore_pending_inserts())
         if not pendings:
-            w(u'<tr><th>&nbsp;</th><td>&nbsp;</td></tr>')
+            w(u'<tr><th>&#160;</th><td>&#160;</td></tr>')
         else:
             for row in pendings:
                 # soon to be linked to entities
@@ -519,7 +519,7 @@
             w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>'
               % (rschema, entity.eid, js, __('add a %s' % targettype)))
             w(u'</div>')
-            w(u'<div class="trame_grise">&nbsp;</div>')
+            w(u'<div class="trame_grise">&#160;</div>')
         w(u'</div>')
 
 
--- a/web/views/ibreadcrumbs.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/ibreadcrumbs.py	Wed Aug 26 14:45:56 2009 +0200
@@ -10,50 +10,55 @@
 
 from logilab.mtconverter import xml_escape
 
+from cubicweb.interfaces import IBreadCrumbs
+from cubicweb.selectors import (one_line_rset, implements, one_etype_rset,
+                                two_lines_rset, any_rset)
+from cubicweb.view import EntityView, Component
 # don't use AnyEntity since this may cause bug with isinstance() due to reloading
-from cubicweb.interfaces import IBreadCrumbs
-from cubicweb.selectors import match_context_prop, one_line_rset, implements
 from cubicweb.entity import Entity
-from cubicweb.view import EntityView
 from cubicweb.common.uilib import cut
-from cubicweb.web.component import EntityVComponent
 
 
 def bc_title(entity):
     textsize = entity.req.property_value('navigation.short-line-size')
     return xml_escape(cut(entity.dc_title(), textsize))
 
+# XXX only provides the component version
 
-class BreadCrumbEntityVComponent(EntityVComponent):
+class BreadCrumbEntityVComponent(Component):
     id = 'breadcrumbs'
-    # register msg not generated since no entity implements IPrevNext in cubicweb itself
+    __select__ = one_line_rset() & implements(IBreadCrumbs)
+
+    property_defs = {
+        _('visible'):  dict(type='Boolean', default=True,
+                            help=_('display the component or not')),
+        }
     title = _('contentnavigation_breadcrumbs')
     help = _('contentnavigation_breadcrumbs_description')
-    __select__ = (one_line_rset() & match_context_prop() & implements(IBreadCrumbs))
-    context = 'navtop'
-    order = 5
-    visible = False
-    separator = u'&nbsp;&gt;&nbsp;'
+    separator = u'&#160;&gt;&#160;'
 
     def call(self, view=None, first_separator=True):
         entity = self.rset.get_entity(0,0)
         path = entity.breadcrumbs(view)
         if path:
-            self.w(u'<span class="pathbar">')
+            self.w(u'<span id="breadcrumbs" class="pathbar">')
             if first_separator:
                 self.w(self.separator)
-            root = path.pop(0)
-            if isinstance(root, Entity):
-                self.w(u'<a href="%s">%s</a>' % (self.req.build_url(root.id),
-                                                 root.dc_type('plural')))
-                self.w(self.separator)
-            self.wpath_part(root, entity, not path)
-            for i, parent in enumerate(path):
-                self.w(self.separator)
-                self.w(u"\n")
-                self.wpath_part(parent, entity, i == len(path) - 1)
+            self.render_breadcrumbs(entity, path)
             self.w(u'</span>')
 
+    def render_breadcrumbs(self, contextentity, path):
+        root = path.pop(0)
+        if isinstance(root, Entity):
+            self.w(u'<a href="%s">%s</a>' % (self.req.build_url(root.id),
+                                             root.dc_type('plural')))
+            self.w(self.separator)
+        self.wpath_part(root, contextentity, not path)
+        for i, parent in enumerate(path):
+            self.w(self.separator)
+            self.w(u"\n")
+            self.wpath_part(parent, contextentity, i == len(path) - 1)
+
     def wpath_part(self, part, contextentity, last=False):
         if isinstance(part, Entity):
             if last and part.eid == contextentity.eid:
@@ -70,10 +75,28 @@
             self.w(cut(unicode(part), textsize))
 
 
-class BreadCrumbComponent(BreadCrumbEntityVComponent):
-    __registry__ = 'components'
-    __select__ = (one_line_rset() & implements(IBreadCrumbs))
-    visible = True
+class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent):
+    __select__ = two_lines_rset() & one_etype_rset() & implements(IBreadCrumbs)
+
+    def render_breadcrumbs(self, contextentity, path):
+        # XXX hack: only display etype name or first non entity path part
+        root = path.pop(0)
+        if isinstance(root, Entity):
+            self.w(u'<a href="%s">%s</a>' % (self.req.build_url(root.id),
+                                             root.dc_type('plural')))
+        else:
+            self.wpath_part(root, entity, not path)
+
+
+class BreadCrumbAnyRSetVComponent(BreadCrumbEntityVComponent):
+    __select__ = any_rset()
+
+    def call(self, view=None, first_separator=True):
+        self.w(u'<span id="breadcrumbs" class="pathbar">')
+        if first_separator:
+            self.w(self.separator)
+        self.w(self.req._('search'))
+        self.w(u'</span>')
 
 
 class BreadCrumbView(EntityView):
--- a/web/views/iprogress.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/iprogress.py	Wed Aug 26 14:45:56 2009 +0200
@@ -167,10 +167,10 @@
     """
     id = 'ic_progress_table_view'
 
-    def call(self):
+    def call(self, columns=None):
         view = self.vreg['views'].select('progress_table_view', self.req,
                                          rset=self.rset)
-        columns = list(view.columns)
+        columns = list(columns or view.columns)
         try:
             columns.remove('project')
         except ValueError:
--- a/web/views/management.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/management.py	Wed Aug 26 14:45:56 2009 +0200
@@ -139,7 +139,7 @@
                 # don't give __delete value to build_url else it will be urlquoted
                 # and this will replace %s by %25s
                 delurl += '&__delete=%s:require_permission:%%s' % entity.eid
-                dellinktempl = u'[<a href="%s" title="%s">-</a>]&nbsp;' % (
+                dellinktempl = u'[<a href="%s" title="%s">-</a>]&#160;' % (
                     xml_escape(delurl), _('delete this permission'))
             else:
                 dellinktempl = None
--- a/web/views/navigation.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/navigation.py	Wed Aug 26 14:45:56 2009 +0200
@@ -40,9 +40,9 @@
                                             self.index_display(start, stop)))
             start = stop + 1
         w(u'<div class="pagination">')
-        w(u'%s&nbsp;' % self.previous_link(params))
-        w(u'[&nbsp;%s&nbsp;]' % u'&nbsp;| '.join(blocklist))
-        w(u'&nbsp;%s' % self.next_link(params))
+        w(u'%s&#160;' % self.previous_link(basepath, params))
+        w(u'[&#160;%s&#160;]' % u'&#160;| '.join(blocklist))
+        w(u'&#160;%s' % self.next_link(basepath, params))
         w(u'</div>')
 
     def index_display(self, start, stop):
@@ -131,18 +131,18 @@
             cell = self.format_link_content(index_display(start), index_display(stop))
             blocklist.append(self.page_link(basepath, params, start, stop, cell))
             start = stop + 1
-        self.write_links(params, blocklist)
+        self.write_links(basepath, params, blocklist)
 
     def format_link_content(self, startstr, stopstr):
         text = u'%s - %s' % (startstr.lower()[:self.nb_chars],
                              stopstr.lower()[:self.nb_chars])
         return xml_escape(text)
 
-    def write_links(self, params, blocklist):
+    def write_links(self, basepath, params, blocklist):
         self.w(u'<div class="pagination">')
-        self.w(u'%s&nbsp;' % self.previous_link(params))
-        self.w(u'[&nbsp;%s&nbsp;]' % u'&nbsp;| '.join(blocklist))
-        self.w(u'&nbsp;%s' % self.next_link(params))
+        self.w(u'%s&#160;' % self.previous_link(basepath, params))
+        self.w(u'[&#160;%s&#160;]' % u'&#160;| '.join(blocklist))
+        self.w(u'&#160;%s' % self.next_link(basepath, params))
         self.w(u'</div>')
 
 
@@ -160,7 +160,7 @@
             # make a link to see them all
             if show_all_option:
                 url = xml_escape(self.build_url(__force_display=1, **params))
-                w(u'<p><a href="%s">%s</a></p>\n'
+                w(u'<span><a href="%s">%s</a></span>\n'
                   % (url, req._('show %s results') % len(rset)))
             rset.limit(offset=start, limit=stop-start, inplace=True)
 
--- a/web/views/old_calendar.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/old_calendar.py	Wed Aug 26 14:45:56 2009 +0200
@@ -30,8 +30,8 @@
 
     # Navigation building methods / views ####################################
 
-    PREV = u'<a href="%s">&lt;&lt;</a>&nbsp;&nbsp;<a href="%s">&lt;</a>'
-    NEXT = u'<a href="%s">&gt;</a>&nbsp;&nbsp;<a href="%s">&gt;&gt;</a>'
+    PREV = u'<a href="%s">&lt;&lt;</a>&#160;&#160;<a href="%s">&lt;</a>'
+    NEXT = u'<a href="%s">&gt;</a>&#160;&#160;<a href="%s">&gt;&gt;</a>'
     NAV_HEADER = u"""<table class="calendarPageHeader">
 <tr><td class="prev">%s</td><td class="next">%s</td></tr>
 </table>
@@ -200,7 +200,7 @@
         self.w(u'<tr>')
         rql = self.rset.printable_rql()
         for cur_month in date_range(begin, end, incmonth=1):
-            umonth = u'%s&nbsp;%s' % (self.format_date(cur_month, '%B'), cur_month.year)
+            umonth = u'%s&#160;%s' % (self.format_date(cur_month, '%B'), cur_month.year)
             url = self.build_url(rql=rql, vid=self.id,
                                  year=cur_month.year, month=cur_month.month)
             self.w(u'<th colspan="2"><a href="%s">%s</a></th>' % (xml_escape(url),
@@ -215,7 +215,7 @@
                 else:
                     day = date(cur_month.year, cur_month.month, day_num+1)
                     events = schedule.get(day)
-                    self.w(u'<td>%s&nbsp;%s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), day_num+1))
+                    self.w(u'<td>%s&#160;%s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(), day_num+1))
                     self.format_day_events(day, events)
             self.w(u'</tr>')
 
@@ -345,8 +345,8 @@
             am_row = [am for day, am, pm in row]
             pm_row = [pm for day, am, pm in row]
             formatted_rows.append('<tr>%s%s</tr>'% (week_title, '\n'.join(day_row)))
-            formatted_rows.append('<tr class="amRow"><td>&nbsp;</td>%s</tr>'% '\n'.join(am_row))
-            formatted_rows.append('<tr class="pmRow"><td>&nbsp;</td>%s</tr>'% '\n'.join(pm_row))
+            formatted_rows.append('<tr class="amRow"><td>&#160;</td>%s</tr>'% '\n'.join(am_row))
+            formatted_rows.append('<tr class="pmRow"><td>&#160;</td>%s</tr>'% '\n'.join(pm_row))
         # tigh everything together
         url = self.build_url(rql=rql, vid='ampmcalendarmonth',
                              year=first_day.year, month=first_day.month)
@@ -364,7 +364,7 @@
         self.w(u'<tr>')
         rql = self.rset.printable_rql()
         for cur_month in date_range(begin, end, incmonth=1):
-            umonth = u'%s&nbsp;%s' % (self.format_date(cur_month, '%B'), cur_month.year)
+            umonth = u'%s&#160;%s' % (self.format_date(cur_month, '%B'), cur_month.year)
             url = self.build_url(rql=rql, vid=self.id,
                                  year=cur_month.year, month=cur_month.month)
             self.w(u'<th colspan="3"><a href="%s">%s</a></th>' % (xml_escape(url),
@@ -379,7 +379,7 @@
                 else:
                     day = date(cur_month.year, cur_month.month, day_num+1)
                     events = schedule.get(day)
-                    self.w(u'<td>%s&nbsp;%s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(),
+                    self.w(u'<td>%s&#160;%s</td>\n' % (_(WEEKDAYS[day.weekday()])[0].upper(),
                                                        day_num+1))
                     self.format_day_events(day, events)
             self.w(u'</tr>')
@@ -437,8 +437,8 @@
             am_row = [am for day, am, pm in row]
             pm_row = [pm for day, am, pm in row]
             formatted_rows.append('<tr>%s%s</tr>'% (week_title, '\n'.join(day_row)))
-            formatted_rows.append('<tr class="amRow"><td>&nbsp;</td>%s</tr>'% '\n'.join(am_row))
-            formatted_rows.append('<tr class="pmRow"><td>&nbsp;</td>%s</tr>'% '\n'.join(pm_row))
+            formatted_rows.append('<tr class="amRow"><td>&#160;</td>%s</tr>'% '\n'.join(am_row))
+            formatted_rows.append('<tr class="pmRow"><td>&#160;</td>%s</tr>'% '\n'.join(pm_row))
         # tigh everything together
         url = self.build_url(rql=rql, vid='ampmcalendarmonth',
                              year=first_day.year, month=first_day.month)
@@ -464,7 +464,7 @@
             monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
             w(u'<tr>%s</tr>' % (
                 WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink)))
-            w(u'<tr><th>%s</th><th>&nbsp;</th></tr>'% _(u'Date'))
+            w(u'<tr><th>%s</th><th>&#160;</th></tr>'% _(u'Date'))
             for day in date_range(monday, sunday):
                 events = schedule.get(day)
                 style = day.weekday() % 2 and "even" or "odd"
@@ -534,9 +534,9 @@
 AMPM_CONTENT = u'<td class="%s"><span class="cellTitle">%s</span><div class="cellContent">%s</div></td>'
 
 WEEK_TITLE = u'<th class="weekTitle" colspan="2">%s %s (%s)</th>'
-WEEK_EMPTY_CELL = u'<td class="weekEmptyCell">&nbsp;</td>'
+WEEK_EMPTY_CELL = u'<td class="weekEmptyCell">&#160;</td>'
 WEEK_CELL = u'<td class="weekCell"><div class="cellContent">%s</div></td>'
 
-AMPM_DAYWEEK_EMPTY = u'<td>%s&nbsp;%s</td>'
-AMPM_DAYWEEK = u'<td rowspan="%d">%s&nbsp;%s</td>'
+AMPM_DAYWEEK_EMPTY = u'<td>%s&#160;%s</td>'
+AMPM_DAYWEEK = u'<td rowspan="%d">%s&#160;%s</td>'
 AMPM_WEEK_CELL = u'<td class="ampmWeekCell"><div class="cellContent">%02d:%02d - %s</div></td>'
--- a/web/views/startup.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/startup.py	Wed Aug 26 14:45:56 2009 +0200
@@ -52,10 +52,10 @@
             self.wview('inlined', rset, row=0)
         else:
             self.entities()
-            self.w(u'<div class="hr">&nbsp;</div>')
+            self.w(u'<div class="hr">&#160;</div>')
             self.startup_views()
         if manager and 'Card' in self.schema:
-            self.w(u'<div class="hr">&nbsp;</div>')
+            self.w(u'<div class="hr">&#160;</div>')
             if rset:
                 href = rset.get_entity(0, 0).absolute_url(vid='edition')
                 label = self.req._('edit the index page')
@@ -104,7 +104,7 @@
                        key=lambda (l,a,e):unormalize(l))
         q, r = divmod(len(infos), 2)
         if r:
-            infos.append( (None, '&nbsp;', '&nbsp;') )
+            infos.append( (None, '&#160;', '&#160;') )
         infos = zip(infos[:q+r], infos[q+r:])
         for (_, etypelink, addlink), (_, etypelink2, addlink2) in infos:
             self.w(u'<tr>\n')
@@ -126,7 +126,7 @@
             label = display_name(req, etype, 'plural')
             nb = req.execute('Any COUNT(X) WHERE X is %s' % etype)[0][0]
             url = self.build_url(etype)
-            etypelink = u'&nbsp;<a href="%s">%s</a> (%d)' % (
+            etypelink = u'&#160;<a href="%s">%s</a> (%d)' % (
                 xml_escape(url), label, nb)
             yield (label, etypelink, self.add_entity_link(eschema, req))
 
--- a/web/views/tableview.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/tableview.py	Wed Aug 26 14:45:56 2009 +0200
@@ -51,7 +51,7 @@
         """display a form to filter table's content. This should only
         occurs when a context eid is given
         """
-        self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.formfilter.js'))
+        self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.facets.js'))
         # drop False / None values from vidargs
         vidargs = dict((k, v) for k, v in vidargs.iteritems() if v)
         self.w(u'<form method="post" cubicweb:facetargs="%s" action="">' %
@@ -129,8 +129,7 @@
         # replace the inner div, so don't regenerate everything under the if
         # below
         if not fromformfilter:
-            div_class = 'section'
-            self.w(u'<div class="%s">' % div_class)
+            self.w(u'<div class="section">')
             if not title and 'title' in req.form:
                 title = req.form['title']
             if title:
--- a/web/views/timetable.py	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/views/timetable.py	Wed Aug 26 14:45:56 2009 +0200
@@ -133,7 +133,7 @@
         """ render column headers """
         self.w(u'<tr class="header">\n')
 
-        self.w(u'<th class="ttdate">&nbsp;</th>\n')
+        self.w(u'<th class="ttdate">&#160;</th>\n')
         columns = []
         for user, width in zip(users, widths):
             self.w(u'<th colspan="%s">' % max(MIN_COLS, width))
@@ -191,13 +191,13 @@
                         task_descr, first_row = value
                         if first_row:
                             url = xml_escape(task_descr.task.absolute_url(vid="edition"))
-                            self.w(u'<td rowspan="%d" class="%s %s" onclick="document.location=\'%s\'">&nbsp;<div>' % (
+                            self.w(u'<td rowspan="%d" class="%s %s" onclick="document.location=\'%s\'">&#160;<div>' % (
                                 task_descr.lines, task_descr.color, filled_klasses[kj], url))
                             task_descr.task.view('tooltip', w=self.w)
                             self.w(u'</div></td>')
                     else:
                         if empty_line:
-                            self.w(u'<td class="ttempty">&nbsp;</td>')
+                            self.w(u'<td class="ttempty">&#160;</td>')
                         else:
-                            self.w(u'<td class="%s">&nbsp;</td>' % empty_klasses[kj] )
+                            self.w(u'<td class="%s">&#160;</td>' % empty_klasses[kj] )
             self.w(u'</tr>\n')
--- a/web/wdoc/ChangeLog_en	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/wdoc/ChangeLog_en	Wed Aug 26 14:45:56 2009 +0200
@@ -1,27 +1,34 @@
 .. -*- coding: utf-8 -*-
-.. _`user preferences`: myprefs#fieldset_ui
-
-2008-09-25  --  2.50.0
-    * jQuery replaces MochiKit
-    * schema inheritance support
+.. _`user preferences`: myprefs
+.. _here: sparql
+.. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/
+.. _schema: schema
+.. _OWL: http://www.w3.org/TR/owl-features/
 
-2008-05-13  --  2.48.0
-    * web pages are now served with the ``xhtml+xml`` content type
+2009-08-07  --  3.4.0
+
+    * support for SPARQL_. Click here_ to test it.
 
-2008-03-27  --  2.47.0
-    * fckeditor is now integrated to edit rich text fields. If you don't see it,
-      check your `user preferences`_.
+    * and another step toward the semantic web: new `ExternalUri` entity type
+      with its associated `same_as` relation. See
+      http://www.w3.org/TR/owl-ref/#sameAs-def for more information and check
+      this instance schema_ to see on which entity types it may be applied
 
-2008-03-13  --  2.46.0
-    * new calendar and timetable views.
-    
-    * click-and-edit functionalities : if you see the text edit cursor when
-      you're over a fied, try to double-click!
-      
-    * automatic facets oriented search : a filter box should appear when you're
-      looking for something and more than one entity are displayed.
+    * new "view workflow" and "view history" items in the workflow
+      sub-menu of the actions box
+
+    * you can now edit comments of workflow transition afterward (if authorized,
+      of course)
+
+    * modification date of an entity is updated when its state is changed
+
 
-2008-02-15  --  2.44.0
-    * new internationalized online help system. Click the question mark on the
-      right top corner! Hopefuly some new documentation will appear as time is
-      going. 
+2009-02-26  --  3.1.0
+
+    * schema may be exported as OWL_
+
+    * new ajax interface for site configuration / `user preferences`_
+
+
+2008-10-24  --  2.99.0
+    * cubicweb is now open source !
--- a/web/wdoc/ChangeLog_fr	Fri Aug 21 16:26:20 2009 +0200
+++ b/web/wdoc/ChangeLog_fr	Wed Aug 26 14:45:56 2009 +0200
@@ -1,31 +1,34 @@
 .. -*- coding: utf-8 -*-
 .. _`préférences utilisateurs`: myprefs#fieldset_ui
-
-2008-09-25  --  2.50.0
-    * jQuery remplace MochiKit
-    * support de l'héritage de schéma
+.. _ici: sparql
+.. _SPARQL: http://www.w3.org/TR/rdf-sparql-query/
+.. _schema: schema
+.. _OWL: http://www.w3.org/TR/owl-features/
 
-2008-05-13  --  2.48.0
-    * les pages sont servies en tant que ``xhtml+xml`` pour certains navigateurs
+2009-08-07  --  3.4.0
 
-2008-03-27  --  2.47.0
-    * fckeditor est enfin intégré pour éditer les champs de type texte riche. Si
-      vous ne le voyez pas apparaître, vérifiez vos `préférences utilisateurs`_.
+    * support de SPARQL_. Cliquez ici_ pour le tester.
 
-2008-03-13  --  2.46.0
-    * nouvelle vues calendrier et emploi du temps
-    
-    * fonctionalité "click-et-édite" : si vous voyez apparaitre le curseur
-      d'édition de texte en survolant un champ, essayez de double-cliquer !
-      
-    * recherche par facettes : une boîte de filtrage devrait apparaitre
-      automatiquement lorsque vous effectuez une recherche qui ramène plus d'une
-      entité
+    * et encore un pas vers le web sémantique : un nouveau type d'entité
+      `ExternalUri` et la relation associée `same_as`. Voir
+      http://www.w3.org/TR/owl-ref/#sameAs-def pour plus d'information, ainsi
+      que le schema_ de cette instance pour voir à quels types d'entités cela
+      s'applique.
 
-2008-02-15  --  2.44.0
-    * nouveau système d'aide internationalisé. Cliquez sur le point
-      d'interrogation en haut à droite. Reste à enrichir le contenu de cette
-      documentation, mais cela devrait arriver avec le temps.
+    * nouveau liens "voir les états possibles" et "voir l'historique" dans le sous-menu
+      workflow de la boite actions
+
+    * vous pouvez dorénavant éditer les commentaires des passages de transition
+      depuis l'historique, pour peu que vous ayez les droits nécessaire bien sûr
+
+    * la date de modification d'une entité est mise à jour lorsque son état est changé
 
 
-      
+2009-02-26  --  3.1.0
+
+    * le schéma peut être exporté en OWL_
+
+    * nouvelle interface ajax pour la configuration du site et les `préférences utilisateurs`_
+
+
+