backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 05 Nov 2010 17:11:40 +0100
changeset 6689 b00f31b3b045
parent 6666 55a94beb521d (current diff)
parent 6688 51ddb4842c56 (diff)
child 6712 3e6cd6048be8
backport stable
--- a/cwctl.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/cwctl.py	Fri Nov 05 17:11:40 2010 +0100
@@ -235,9 +235,9 @@
                     tinfo = cwcfg.cube_pkginfo(cube)
                     tversion = tinfo.version
                     cfgpb.add_cube(cube, tversion)
-                except ConfigurationError:
+                except ConfigurationError, ex:
                     tinfo = None
-                    tversion = '[missing cube information]'
+                    tversion = '[missing cube information: %s]' % ex
                 print '* %s %s' % (cube.ljust(namesize), tversion)
                 if self.config.verbose:
                     if tinfo:
--- a/devtools/repotest.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/devtools/repotest.py	Fri Nov 05 17:11:40 2010 +0100
@@ -22,6 +22,7 @@
 
 __docformat__ = "restructuredtext en"
 
+from copy import deepcopy
 from pprint import pprint
 
 from logilab.common.decorators import clear_cache
@@ -388,6 +389,11 @@
                 return x.value
     return _orig_choose_term(self, DumbOrderedDict2(sourceterms, get_key))
 
+from cubicweb.server.sources.pyrorql import PyroRQLSource
+_orig_syntax_tree_search = PyroRQLSource.syntax_tree_search
+
+def _syntax_tree_search(*args, **kwargs):
+    return deepcopy(_orig_syntax_tree_search(*args, **kwargs))
 
 def do_monkey_patch():
     RQLRewriter.insert_snippets = _insert_snippets
@@ -397,6 +403,7 @@
     ExecutionPlan.init_temp_table = _init_temp_table
     PartPlanInformation.merge_input_maps = _merge_input_maps
     PartPlanInformation._choose_term = _choose_term
+    PyroRQLSource.syntax_tree_search = _syntax_tree_search
 
 def undo_monkey_patch():
     RQLRewriter.insert_snippets = _orig_insert_snippets
@@ -405,3 +412,4 @@
     ExecutionPlan.init_temp_table = _orig_init_temp_table
     PartPlanInformation.merge_input_maps = _orig_merge_input_maps
     PartPlanInformation._choose_term = _orig_choose_term
+    PyroRQLSource.syntax_tree_search = _orig_syntax_tree_search
--- a/devtools/testlib.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/devtools/testlib.py	Fri Nov 05 17:11:40 2010 +0100
@@ -661,6 +661,13 @@
 
     # content validation #######################################################
 
+    def assertDocTestFile(self, testfile):
+        # doctest returns tuple (failure_count, test_count)
+        result = self.shell.process_script(testfile)
+        if result[0] and result[1]:
+            raise self.failureException("doctest file '%s' failed"
+                                        % testfile)
+
     # validators are used to validate (XML, DTD, whatever) view's content
     # validators availables are :
     #  DTDValidator : validates XML + declared DTD
--- a/migration.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/migration.py	Fri Nov 05 17:11:40 2010 +0100
@@ -358,8 +358,10 @@
             self.commit()
         else: # script_mode == 'doctest'
             import doctest
-            doctest.testfile(migrscript, module_relative=False,
-                             optionflags=doctest.ELLIPSIS, globs=scriptlocals)
+            return doctest.testfile(migrscript, module_relative=False,
+                                    optionflags=doctest.ELLIPSIS,
+                                    encoding='utf-8',
+                                    globs=scriptlocals)
         self._context_stack.pop()
 
     def cmd_option_renamed(self, oldname, newname):
--- a/misc/migration/3.10.4_Any.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/misc/migration/3.10.4_Any.py	Fri Nov 05 17:11:40 2010 +0100
@@ -1,6 +1,6 @@
 for eschema in schema.entities():
     if not (eschema.final or 'cw_source' in eschema.subjrels):
-        add_relation_definition(eschema, 'cw_source', 'CWSource')
+        add_relation_definition(eschema.type, 'cw_source', 'CWSource')
 
 sql('INSERT INTO cw_source_relation(eid_from, eid_to) '
     'SELECT e.eid,s.cw_eid FROM entities as e, cw_CWSource as s '
--- a/server/migractions.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/server/migractions.py	Fri Nov 05 17:11:40 2010 +0100
@@ -1461,7 +1461,7 @@
         return res
 
     def rqliter(self, rql, kwargs=None, ask_confirm=True):
-        return ForRqlIterator(self, rql, None, ask_confirm)
+        return ForRqlIterator(self, rql, kwargs, ask_confirm)
 
     # broken db commands ######################################################
 
--- a/server/msplanner.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/server/msplanner.py	Fri Nov 05 17:11:40 2010 +0100
@@ -745,6 +745,15 @@
                                                and self._need_ext_source_access(term, rel):
                                             self.needsplit = True
                                             return
+        else:
+            # remove sources only accessing to constant nodes
+            for source, terms in self._sourcesterms.items():
+                if source is self.system_source:
+                    continue
+                if not any(x for x in terms if not isinstance(x, Constant)):
+                    del self._sourcesterms[source]
+            if len(self._sourcesterms) < 2:
+                self.needsplit = False
 
     @cached
     def _need_ext_source_access(self, var, rel):
--- a/server/repository.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/server/repository.py	Fri Nov 05 17:11:40 2010 +0100
@@ -1120,9 +1120,13 @@
                     rql = 'DELETE Y %s X WHERE X eid %%(x)s' % rtype
                 if scleanup:
                     # source cleaning: only delete relations stored locally
-                    rql += ', NOT Y cw_source S, S name %(source)s'
-                session.execute(rql, {'x': eid, 'source': sourceuri},
-                                build_descr=False)
+                    rql += ', NOT (Y cw_source S, S name %(source)s)'
+                try:
+                    session.execute(rql, {'x': eid, 'source': sourceuri},
+                                    build_descr=False)
+                except:
+                    self.exception('error while cascading delete for entity %s '
+                                   'from %s. RQL: %s', entity, sourceuri, rql)
         self.system_source.delete_info(session, entity, sourceuri, extid)
 
     def locate_relation_source(self, session, subject, rtype, object):
--- a/server/serverconfig.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/server/serverconfig.py	Fri Nov 05 17:11:40 2010 +0100
@@ -297,7 +297,12 @@
                 _sconfig = SourceConfiguration(
                     self, options=SOURCE_TYPES['native'].options)
                 for attr, val in sconfig.items():
-                    _sconfig.set_option(attr, val)
+                    try:
+                        _sconfig.set_option(attr, val)
+                    except lgconfig.OptionError:
+                        # skip adapter, may be present on pre 3.10 instances
+                        if attr != 'adapter':
+                            self.error('skip unknown option %s in sources file')
                 sconfig = _sconfig
             print >> stream, '[%s]' % section
             print >> stream, generate_source_config(sconfig)
--- a/server/sources/pyrorql.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/server/sources/pyrorql.py	Fri Nov 05 17:11:40 2010 +0100
@@ -255,7 +255,7 @@
         try:
             for etype, extid in modified:
                 try:
-                    eid = self.local_eid(cnx, extid, session)
+                    eid = self.local_eid(cnx, extid, session)[0]
                     if eid is not None:
                         rset = session.eid_rset(eid, etype)
                         entity = rset.get_entity(0, 0)
--- a/server/test/unittest_msplanner.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/server/test/unittest_msplanner.py	Fri Nov 05 17:11:40 2010 +0100
@@ -811,7 +811,8 @@
         # use a guest user
         self.session = self.user_groups_session('guests')
         ueid = self.session.user.eid
-        # note: same as the above query but because of the subquery usage, the display differs (not printing solutions for each union)
+        # note: same as the above query but because of the subquery usage, the
+        # display differs (not printing solutions for each union)
         self._test('Any X LIMIT 10 OFFSET 10 WHERE X has_text "bla"',
                    [('FetchStep', [('Any E WHERE E type "X", E is Note', [{'E': 'Note'}])],
                       [self.cards, self.system], None, {'E': 'table1.C0'}, []),
@@ -1709,6 +1710,18 @@
                     ],
                    {'x': ueid, 'y': ueid})
 
+    def test_delete_relation3(self):
+        repo._type_source_cache[999999] = ('Note', 'cards', 999999)
+        self._test('DELETE Y multisource_inlined_rel X WHERE X eid %(x)s, NOT (Y cw_source S, S name %(source)s)',
+                   [('DeleteRelationsStep',
+                     [('OneFetchStep',
+                       [('Any Y,999999 WHERE Y multisource_inlined_rel 999999, NOT EXISTS(Y cw_source S, S name "cards"), S is CWSource, Y is IN(Card, Note)',
+                         [{'S': 'CWSource', 'Y': 'Card'}, {'S': 'CWSource', 'Y': 'Note'}])],
+                       None, None, [self.system], {},
+                       [])]
+                     )],
+                   {'x': 999999, 'source': 'cards'})
+
     def test_delete_entity1(self):
         repo._type_source_cache[999999] = ('Note', 'system', 999999)
         self._test('DELETE Note X WHERE X eid %(x)s, NOT Y multisource_rel X',
@@ -1926,7 +1939,6 @@
 
     def test_source_specified_3_2(self):
         self.skipTest('oops')
-        self.set_debug('DBG_MS')
         self._test('Any STN WHERE X is Note, X type XT, X in_state ST, ST name STN, X cw_source S, S name "cards"',
                    [('OneFetchStep',
                      [('Any X,XT WHERE X is Card, X title XT',
--- a/test/unittest_entity.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/test/unittest_entity.py	Fri Nov 05 17:11:40 2010 +0100
@@ -24,6 +24,8 @@
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.mttransforms import HAS_TAL
 from cubicweb.entities import fetch_config
+from cubicweb.uilib import soup2xhtml
+
 
 class EntityTC(CubicWebTC):
 
@@ -412,24 +414,14 @@
         self.assertEqual(e.printable_value('content'), u'hop\nhop\nhip\nmomo')
 
     def test_printable_value_bad_html_ms(self):
-        self.skipTest('fix soup2xhtml to handle this test')
         req = self.request()
         e = req.create_entity('Card', title=u'bad html', content=u'<div>R&D<br>',
                             content_format=u'text/html')
         tidy = lambda x: x.replace('\n', '')
         e.cw_attr_cache['content'] = u'<div x:foo="bar">ms orifice produces weird html</div>'
-        self.assertEqual(tidy(e.printable_value('content')),
-                          u'<div>ms orifice produces weird html</div>')
-        import tidy as tidymod # apt-get install python-tidy
-        tidy = lambda x: str(tidymod.parseString(x.encode('utf-8'),
-                                                 **{'drop_proprietary_attributes': True,
-                                                    'output_xhtml': True,
-                                                    'show_body_only' : True,
-                                                    'quote-nbsp' : False,
-                                                    'char_encoding' : 'utf8'})).decode('utf-8').strip()
-        self.assertEqual(tidy(e.printable_value('content')),
-                          u'<div>ms orifice produces weird html</div>')
-
+        # Caution! current implementation of soup2xhtml strips first div element
+        content = soup2xhtml(e.printable_value('content'), 'utf-8')
+        self.assertMultiLineEqual(content, u'<div>ms orifice produces weird html</div>')
 
     def test_fulltextindex(self):
         e = self.vreg['etypes'].etype_class('File')(self.request())
--- a/test/unittest_uilib.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/test/unittest_uilib.py	Fri Nov 05 17:11:40 2010 +0100
@@ -20,10 +20,16 @@
 
 __docformat__ = "restructuredtext en"
 
+
+import pkg_resources
 from logilab.common.testlib import TestCase, unittest_main
+from unittest2 import skipIf
 
 from cubicweb import uilib
 
+lxml_version = pkg_resources.get_distribution('lxml').version.split('.')
+
+
 class UILIBTC(TestCase):
 
     def test_remove_tags(self):
@@ -91,7 +97,15 @@
             got = uilib.text_cut(text, 30)
             self.assertEqual(got, expected)
 
+    def test_soup2xhtml_0(self):
+        self.assertEqual(uilib.soup2xhtml('hop\r\nhop', 'ascii'),
+                          'hop\nhop')
+
     def test_soup2xhtml_1_1(self):
+        self.assertEqual(uilib.soup2xhtml('hop', 'ascii'),
+                          'hop')
+        self.assertEqual(uilib.soup2xhtml('hop<div>', 'ascii'),
+                          'hop<div/>')
         self.assertEqual(uilib.soup2xhtml('hop <div>', 'ascii'),
                           'hop <div/>')
         self.assertEqual(uilib.soup2xhtml('<div> hop', 'ascii'),
@@ -115,11 +129,14 @@
         self.assertEqual(uilib.soup2xhtml('hop <body> hop', 'ascii'),
                           'hop  hop')
 
-    def test_soup2xhtml_2_2(self):
+    def test_soup2xhtml_2_2a(self):
         self.assertEqual(uilib.soup2xhtml('hop </body>', 'ascii'),
                           'hop ')
         self.assertEqual(uilib.soup2xhtml('</body> hop', 'ascii'),
                           ' hop')
+
+    @skipIf(lxml_version < ['2', '2'], 'expected behaviour on recent version of lxml only')
+    def test_soup2xhtml_2_2b(self):
         self.assertEqual(uilib.soup2xhtml('hop </body> hop', 'ascii'),
                           'hop  hop')
 
@@ -139,6 +156,10 @@
         self.assertEqual(uilib.soup2xhtml('hop </html> hop', 'ascii'),
                           'hop  hop')
 
+    def test_soup2xhtml_3_3(self):
+        self.assertEqual(uilib.soup2xhtml('<script>test</script> hop ', 'ascii'),
+                          ' hop ')
+
     def test_js(self):
         self.assertEqual(str(uilib.js.pouet(1, "2")),
                           'pouet(1,"2")')
@@ -147,6 +168,23 @@
         self.assertEqual(str(uilib.js.cw.pouet(1, "2").pouet(None)),
                           'cw.pouet(1,"2").pouet(null)')
 
+    def test_embedded_css(self):
+        incoming = u"""voir le ticket <style type="text/css">@font-face { font-family: "Cambria"; }p.MsoNormal, li.MsoNormal, div.MsoNormal { margin: 0cm 0cm 10pt; font-size: 12pt; font-family: "Times New Roman"; }a:link, span.MsoHyperlink { color: blue; text-decoration: underline; }a:visited, span.MsoHyperlinkFollowed { color: purple; text-decoration: underline; }div.Section1 { page: Section1; }</style></p><p class="MsoNormal">text</p>"""
+        expected = 'voir le ticket <p class="MsoNormal">text</p>'
+        self.assertMultiLineEqual(uilib.soup2xhtml(incoming, 'ascii'), expected)
+
+    def test_unknown_namespace(self):
+        incoming = '''<table cellspacing="0" cellpadding="0" width="81" border="0" x:str="" style="width: 61pt; border-collapse: collapse">
+<colgroup><col width="81" style="width: 61pt; mso-width-source: userset; mso-width-alt: 2962"/></colgroup>
+<tbody><tr height="17" style="height: 12.75pt"><td width="81" height="17" style="border-right: #e0dfe3; border-top: #e0dfe3; border-left: #e0dfe3; width: 61pt; border-bottom: #e0dfe3; height: 12.75pt; background-color: transparent"><font size="2">XXXXXXX</font></td></tr></tbody>
+</table>'''
+        expected = '''<table cellspacing="0" cellpadding="0" width="81" border="0">\
+<colgroup><col width="81"/></colgroup>\
+<tbody><tr height="17"><td width="81" height="17">XXXXXXX</td></tr></tbody>\
+</table>'''
+        self.assertMultiLineEqual(uilib.soup2xhtml(incoming, 'ascii'), expected)
+
+
 if __name__ == '__main__':
     unittest_main()
 
--- a/uilib.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/uilib.py	Fri Nov 05 17:11:40 2010 +0100
@@ -109,12 +109,6 @@
         return u''
     return REF_PROG.sub(lambda obj, view=view:_subst_rql(view, obj), text)
 
-# fallback implementation, nicer one defined below if lxml is available
-def soup2xhtml(data, encoding):
-    # normalize line break
-    # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
-    return u'\n'.join(data.splitlines())
-
 # fallback implementation, nicer one defined below if lxml> 2.0 is available
 def safe_cut(text, length):
     """returns a string of length <length> based on <text>, removing any html
@@ -132,27 +126,30 @@
 fallback_safe_cut = safe_cut
 
 REM_ROOT_HTML_TAGS = re.compile('</(body|html)>', re.U)
+
 try:
-    from lxml import etree
-except (ImportError, AttributeError):
-    # gae environment: lxml not available
-    pass
-else:
+    from lxml import etree, html
+    from lxml.html import clean, defs
+
+    ALLOWED_TAGS = (defs.general_block_tags | defs.list_tags | defs.table_tags |
+                    defs.phrase_tags | defs.font_style_tags |
+                    set(('span', 'a', 'br', 'img', 'map', 'area', 'sub', 'sup'))
+                    )
+
+    CLEANER = clean.Cleaner(allow_tags=ALLOWED_TAGS, remove_unknown_tags=False,
+                            style=True, safe_attrs_only=True,
+                            add_nofollow=False,
+                            )
 
     def soup2xhtml(data, encoding):
-        """tidy (at least try) html soup and return the result
-
-        Note: the function considers a string with no surrounding tag as valid
-              if <div>`data`</div> can be parsed by an XML parser
+        """tidy html soup by allowing some element tags and return the result
         """
         # remove spurious </body> and </html> tags, then normalize line break
         # (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1)
         data = REM_ROOT_HTML_TAGS.sub('', u'\n'.join(data.splitlines()))
-        # XXX lxml 1.1 support still needed ?
-        xmltree = etree.HTML('<div>%s</div>' % data)
-        # NOTE: lxml 1.1 (etch platforms) doesn't recognize
-        #       the encoding=unicode parameter (lxml 2.0 does), this is
-        #       why we specify an encoding and re-decode to unicode later
+        xmltree = etree.HTML(CLEANER.clean_html('<div>%s</div>' % data))
+        # NOTE: lxml 2.0 does support encoding='unicode', but last time I (syt)
+        # tried I got weird results (lxml 2.2.8)
         body = etree.tostring(xmltree[0], encoding=encoding)
         # remove <body> and </body> and decode to unicode
         snippet = body[6:-7].decode(encoding)
@@ -163,7 +160,29 @@
             snippet = snippet[5:-6]
         return snippet
 
-    if hasattr(etree.HTML('<div>test</div>'), 'iter'):
+        # lxml.Cleaner envelops text elements by internal logic (not accessible)
+        # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
+        # TODO drop attributes in elements
+        # TODO add policy configuration (content only, embedded content, ...)
+        # XXX this is buggy for "<p>text1</p><p>text2</p>"...
+        # XXX drop these two snippets action and follow the lxml behaviour
+        # XXX (tests need to be updated)
+        # if snippet.startswith('<div>') and snippet.endswith('</div>'):
+        #     snippet = snippet[5:-6]
+        # if snippet.startswith('<p>') and snippet.endswith('</p>'):
+        #     snippet = snippet[3:-4]
+        return snippet.decode(encoding)
+
+except (ImportError, AttributeError):
+    # gae environment: lxml not available
+    # fallback implementation
+    def soup2xhtml(data, encoding):
+        # normalize line break
+        # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
+        return u'\n'.join(data.splitlines())
+else:
+
+    if hasattr(etree.HTML('<div>test</div>'), 'iter'): # XXX still necessary?
 
         def safe_cut(text, length):
             """returns an html document of length <length> based on <text>,
@@ -342,6 +361,16 @@
 
 import traceback
 
+def exc_message(ex, encoding):
+    try:
+        return unicode(ex)
+    except:
+        try:
+            return unicode(str(ex), encoding, 'replace')
+        except:
+            return unicode(repr(ex), encoding, 'replace')
+
+
 def rest_traceback(info, exception):
     """return a ReST formated traceback"""
     res = [u'Traceback\n---------\n::\n']
--- a/utils.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/utils.py	Fri Nov 05 17:11:40 2010 +0100
@@ -201,6 +201,7 @@
     def pop(self, i):
         self._size -= 1
 
+
 class UStringIO(list):
     """a file wrapper which automatically encode unicode string to an encoding
     specifed in the constructor
--- a/web/application.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/web/application.py	Fri Nov 05 17:11:40 2010 +0100
@@ -140,13 +140,7 @@
 
 
 class CookieSessionHandler(object):
-    """a session handler using a cookie to store the session identifier
-
-    :cvar SESSION_VAR:
-      string giving the name of the variable used to store the session
-      identifier
-    """
-    SESSION_VAR = '__session'
+    """a session handler using a cookie to store the session identifier"""
 
     def __init__(self, appli):
         self.vreg = appli.vreg
@@ -180,6 +174,14 @@
         """
         self.session_manager.clean_sessions()
 
+    def session_cookie(self, req):
+        """return a string giving the name of the cookie used to store the
+        session identifier.
+        """
+        if req.https:
+            return '__%s_https_session' % self.vreg.config.appid
+        return '__%s_session' % self.vreg.config.appid
+
     def set_session(self, req):
         """associate a session to the request
 
@@ -193,8 +195,9 @@
         :raise Redirect: if authentication has occurred and succeed
         """
         cookie = req.get_cookie()
+        sessioncookie = self.session_cookie(req)
         try:
-            sessionid = str(cookie[self.SESSION_VAR].value)
+            sessionid = str(cookie[sessioncookie].value)
         except KeyError: # no session cookie
             session = self.open_session(req)
         else:
@@ -206,7 +209,7 @@
                 try:
                     session = self.open_session(req)
                 except AuthenticationError:
-                    req.remove_cookie(cookie, self.SESSION_VAR)
+                    req.remove_cookie(cookie, sessioncookie)
                     raise
 
     def get_session(self, req, sessionid):
@@ -215,10 +218,11 @@
     def open_session(self, req):
         session = self.session_manager.open_session(req)
         cookie = req.get_cookie()
-        cookie[self.SESSION_VAR] = session.sessionid
+        sessioncookie = self.session_cookie(req)
+        cookie[sessioncookie] = session.sessionid
         if req.https and req.base_url().startswith('https://'):
-            cookie[self.SESSION_VAR]['secure'] = True
-        req.set_cookie(cookie, self.SESSION_VAR, maxage=None)
+            cookie[sessioncookie]['secure'] = True
+        req.set_cookie(cookie, sessioncookie, maxage=None)
         if not session.anonymous_session:
             self._postlogin(req)
         return session
@@ -265,7 +269,8 @@
         `AuthenticationError`
         """
         self.session_manager.close_session(req.session)
-        req.remove_cookie(req.get_cookie(), self.SESSION_VAR)
+        sessioncookie = self.session_cookie(req)
+        req.remove_cookie(req.get_cookie(), sessioncookie)
         raise LogOut(url=goto_url)
 
 
--- a/web/facet.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/web/facet.py	Fri Nov 05 17:11:40 2010 +0100
@@ -709,9 +709,18 @@
             subj = self.filtered_variable.name
             obj = utils.rqlvar_maker(defined=self.rqlst.defined_vars,
                                      aliases=self.rqlst.aliases).next()
-        rql = 'Any %s LIMIT 1 WHERE NOT %s %s %s, %s' % (
-            self.filtered_variable.name, subj, self.rtype, obj,
-            self.rqlst.where.as_string())
+        restrictions = []
+        if self.rqlst.where:
+            restrictions.append(self.rqlst.where.as_string())
+        if self.rqlst.with_:
+            restrictions.append('WITH ' + ','.join(
+                term.as_string() for term in self.rqlst.with_))
+        if restrictions:
+            restrictions = ',' + ','.join(restrictions)
+        else:
+            restrictions = ''
+        rql = 'Any %s LIMIT 1 WHERE NOT %s %s %s%s' % (
+            self.filtered_variable.name, subj, self.rtype, obj, restrictions)
         try:
             return bool(self.rqlexec(rql, self.cw_rset and self.cw_rset.args))
         except:
--- a/web/formwidgets.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/web/formwidgets.py	Fri Nov 05 17:11:40 2010 +0100
@@ -888,7 +888,9 @@
         values = {}
         path = req.form.get(field.input_name(form, 'path'))
         if isinstance(path, basestring):
-            path = path.strip() or None
+            path = path.strip()
+        if path is None:
+            path = u''
         fqs = req.form.get(field.input_name(form, 'fqs'))
         if isinstance(fqs, basestring):
             fqs = fqs.strip() or None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_formwidgets.py	Fri Nov 05 17:11:40 2010 +0100
@@ -0,0 +1,44 @@
+# copyright 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""unittests for cw.web.formwidgets"""
+
+from logilab.common.testlib import TestCase, unittest_main, mock_object as mock
+
+from cubicweb.devtools import TestServerConfiguration, fake
+from cubicweb.web import uicfg, formwidgets, formfields
+
+from cubes.file.entities import File
+
+def setup_module(*args):
+    global schema
+    config = TestServerConfiguration('data', apphome=WidgetsTC.datadir)
+    config.bootstrap_cubes()
+    schema = config.load_schema()
+
+class WidgetsTC(TestCase):
+
+    def test_state_fields(self):
+        field = formfields.guess_field(schema['Bookmark'], schema['path'])
+        widget = formwidgets.EditableURLWidget()
+        req = fake.FakeRequest(form={'path-subjectfqs:A': 'param=value&vid=view'})
+        form = mock(_cw=req, formvalues={}, edited_entity=mock(eid='A'))
+        self.assertEqual(widget.process_field_data(form, field),
+                         '?param=value%26vid%3Dview')
+
+if __name__ == '__main__':
+    unittest_main()
--- a/web/views/basecomponents.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/web/views/basecomponents.py	Fri Nov 05 17:11:40 2010 +0100
@@ -80,10 +80,10 @@
     __abstract__ = True
     cw_property_defs = component.override_ctx(
         component.CtxComponent,
-        vocabulary=['header-left', 'header-center', 'header-right'])
+        vocabulary=['header-left', 'header-right'])
     # don't want user to hide this component using an cwproperty
     site_wide = True
-    context = _('header-center')
+    context = _('header-left')
 
 
 class ApplLogo(HeaderComponent):
@@ -99,7 +99,6 @@
 class ApplicationName(HeaderComponent):
     """display the instance name"""
     __regid__ = 'appliname'
-    context = _('header-center')
 
     def render(self, w):
         title = self._cw.property_value('ui.site-title')
--- a/web/views/basecontrollers.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/web/views/basecontrollers.py	Fri Nov 05 17:11:40 2010 +0100
@@ -27,6 +27,7 @@
 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
                       AuthenticationError, typed_eid)
 from cubicweb.utils import UStringIO, json, json_dumps
+from cubicweb.uilib import exc_message
 from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params
 from cubicweb.mail import format_mail
 from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse
@@ -252,6 +253,7 @@
     # we receive unicode keys which is not supported by the **syntax
     return dict((str(key), value) for key, value in extraargs.iteritems())
 
+
 class JSonController(Controller):
     __regid__ = 'json'
 
@@ -281,15 +283,15 @@
         except ValueError, exc:
             self.exception('error while decoding json arguments for js_%s: %s',
                            fname, args, exc)
-            raise RemoteCallFailed(repr(exc))
+            raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
         try:
             result = func(*args)
         except (RemoteCallFailed, DirectResponse):
             raise
-        except Exception, ex:
+        except Exception, exc:
             self.exception('an exception occurred while calling js_%s(%s): %s',
-                           fname, args, ex)
-            raise RemoteCallFailed(repr(ex))
+                           fname, args, exc)
+            raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
         if result is None:
             return ''
         # get unicode on @htmlize methods, encoded string on @jsonize methods
--- a/web/views/basetemplates.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/web/views/basetemplates.py	Fri Nov 05 17:11:40 2010 +0100
@@ -339,8 +339,7 @@
         """build the top menu with authentification info and the rql box"""
         w = self.w
         w(u'<table id="header"><tr>\n')
-        for colid, context in (('firstcolumn', 'header-left'),
-                               ('headtext', 'header-center'),
+        for colid, context in (('headtext', 'header-left'),
                                ('header-right', 'header-right'),
                                ):
             w(u'<td id="%s">' % colid)
--- a/web/views/bookmark.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/web/views/bookmark.py	Fri Nov 05 17:11:40 2010 +0100
@@ -24,8 +24,8 @@
 
 from cubicweb import Unauthorized
 from cubicweb.selectors import is_instance, one_line_rset
-from cubicweb.web.htmlwidgets import BoxWidget, BoxMenu, RawBoxItem
-from cubicweb.web import action, component, uicfg, formwidgets as fw
+from cubicweb.web import (action, component, uicfg, htmlwidgets,
+                          formwidgets as fw)
 from cubicweb.web.views import primary
 
 _abaa = uicfg.actionbox_appearsin_addmenu
@@ -105,7 +105,7 @@
                 label = '<div>%s %s</div>' % (dlink, label)
             self.append(label)
         if self.can_edit:
-            menu = BoxMenu(req._('manage bookmarks'))
+            menu = htmlwidgets.BoxMenu(req._('manage bookmarks'))
             linkto = 'bookmarked_by:%s:subject' % ueid
             # use a relative path so that we can move the instance without
             # loosing bookmarks
--- a/web/views/boxes.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/web/views/boxes.py	Fri Nov 05 17:11:40 2010 +0100
@@ -170,9 +170,10 @@
     """display a box containing links to all possible views"""
     __regid__ = 'possible_views_box'
 
-    visible = False
+    contextual = True
     title = _('possible views')
     order = 10
+    visible = False # disabled by default
 
     def init_rendering(self):
         self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw,
@@ -195,9 +196,10 @@
     """display a box containing links to all startup views"""
     __regid__ = 'startup_views_box'
 
-    visible = False # disabled by default
+    contextual = False
     title = _('startup views')
     order = 70
+    visible = False # disabled by default
 
     def init_rendering(self):
         self.views = [v for v in self._cw.vreg['views'].possible_views(self._cw)
--- a/web/views/management.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/web/views/management.py	Fri Nov 05 17:11:40 2010 +0100
@@ -24,7 +24,7 @@
 
 from cubicweb.selectors import yes, none_rset, match_user_groups, authenticated_user
 from cubicweb.view import AnyRsetView, StartupView, EntityView, View
-from cubicweb.uilib import html_traceback, rest_traceback
+from cubicweb.uilib import html_traceback, rest_traceback, exc_message
 from cubicweb.web import formwidgets as wdgs
 from cubicweb.web.formfields import guess_field
 from cubicweb.web.views.schema import SecurityViewMixIn
@@ -220,15 +220,6 @@
             form.render(w=w)
 
 
-def exc_message(ex, encoding):
-    try:
-        return unicode(ex)
-    except:
-        try:
-            return unicode(str(ex), encoding, 'replace')
-        except:
-            return unicode(repr(ex), encoding, 'replace')
-
 def text_error_description(ex, excinfo, req, eversion, cubes):
     binfo = rest_traceback(excinfo, xml_escape(ex))
     binfo += u'\n\n:URL: %s\n' % req.url()
--- a/web/views/timetable.py	Wed Nov 03 16:39:49 2010 +0100
+++ b/web/views/timetable.py	Fri Nov 05 17:11:40 2010 +0100
@@ -54,7 +54,7 @@
         for row in xrange(self.cw_rset.rowcount):
             task = self.cw_rset.get_entity(row, 0)
             icalendarable = task.cw_adapt_to('ICalendarable')
-            if len(self.cw_rset[row]) > 1:
+            if len(self.cw_rset[row]) > 1 and self.cw_rset.description[row][1] == 'CWUser':
                 user = self.cw_rset.get_entity(row, 1)
             else:
                 user = ALL_USERS