backport 3.14.4-2 stable
authorDavid Douard <david.douard@logilab.fr>
Thu, 01 Mar 2012 08:55:14 +0100
branchstable
changeset 8284 bca3e67fdba9
parent 8279 fa167153d384 (diff)
parent 8283 ada08505cf12 (current diff)
child 8285 32098d98bf2f
backport 3.14.4-2
--- a/.hgignore	Thu Mar 01 08:51:32 2012 +0100
+++ b/.hgignore	Thu Mar 01 08:55:14 2012 +0100
@@ -14,6 +14,7 @@
 .*/data/database/.*\.sqlite
 .*/data/database/.*\.config
 .*/data/database/tmpdb.*
+.*/data/ldapdb/.*
 ^doc/html/
 ^doc/doctrees/
 ^doc/book/en/devweb/js_api/
--- a/MANIFEST.in	Thu Mar 01 08:51:32 2012 +0100
+++ b/MANIFEST.in	Thu Mar 01 08:55:14 2012 +0100
@@ -12,7 +12,7 @@
 include web/views/*.pt
 recursive-include web/data external_resources *.js *.css *.py *.png *.gif *.ico *.ttf
 recursive-include web/wdoc *.rst *.png *.xml ChangeLog*
-recursive-include devtools/data *.js *.css
+recursive-include devtools/data *.js *.css *.sh
 
 recursive-include i18n *.pot *.po
 recursive-include schemas *.py *.sql
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/data/xvfb-run.sh	Thu Mar 01 08:55:14 2012 +0100
@@ -0,0 +1,191 @@
+#!/bin/sh
+
+# $Id: xvfb-run 2027 2004-11-16 14:54:16Z branden $
+
+# This script starts an instance of Xvfb, the "fake" X server, runs a command
+# with that server available, and kills the X server when done.  The return
+# value of the command becomes the return value of this script.
+#
+# If anyone is using this to build a Debian package, make sure the package
+# Build-Depends on xvfb, xbase-clients, and xfonts-base.
+
+set -e
+
+PROGNAME=xvfb-run
+SERVERNUM=99
+AUTHFILE=
+ERRORFILE=/dev/null
+STARTWAIT=3
+XVFBARGS="-screen 0 640x480x8"
+LISTENTCP="-nolisten tcp"
+XAUTHPROTO=.
+
+# Query the terminal to establish a default number of columns to use for
+# displaying messages to the user.  This is used only as a fallback in the event
+# the COLUMNS variable is not set.  ($COLUMNS can react to SIGWINCH while the
+# script is running, and this cannot, only being calculated once.)
+DEFCOLUMNS=$(stty size 2>/dev/null | awk '{print $2}') || true
+if ! expr "$DEFCOLUMNS" : "[[:digit:]]\+$" >/dev/null 2>&1; then
+    DEFCOLUMNS=80
+fi
+
+# Display a message, wrapping lines at the terminal width.
+message () {
+    echo "$PROGNAME: $*" | fmt -t -w ${COLUMNS:-$DEFCOLUMNS}
+}
+
+# Display an error message.
+error () {
+    message "error: $*" >&2
+}
+
+# Display a usage message.
+usage () {
+    if [ -n "$*" ]; then
+        message "usage error: $*"
+    fi
+    cat <<EOF
+Usage: $PROGNAME [OPTION ...] COMMAND
+Run COMMAND (usually an X client) in a virtual X server environment.
+Options:
+-a        --auto-servernum          try to get a free server number, starting at
+                                    --server-num
+-e FILE   --error-file=FILE         file used to store xauth errors and Xvfb
+                                    output (default: $ERRORFILE)
+-f FILE   --auth-file=FILE          file used to store auth cookie
+                                    (default: ./.Xauthority)
+-h        --help                    display this usage message and exit
+-n NUM    --server-num=NUM          server number to use (default: $SERVERNUM)
+-l        --listen-tcp              enable TCP port listening in the X server
+-p PROTO  --xauth-protocol=PROTO    X authority protocol name to use
+                                    (default: xauth command's default)
+-s ARGS   --server-args=ARGS        arguments (other than server number and
+                                    "-nolisten tcp") to pass to the Xvfb server
+                                    (default: "$XVFBARGS")
+-w DELAY  --wait=DELAY              delay in seconds to wait for Xvfb to start
+                                    before running COMMAND (default: $STARTWAIT)
+EOF
+}
+
+# Find a free server number by looking at .X*-lock files in /tmp.
+find_free_servernum() {
+    # Sadly, the "local" keyword is not POSIX.  Leave the next line commented in
+    # the hope Debian Policy eventually changes to allow it in /bin/sh scripts
+    # anyway.
+    #local i
+
+    i=$SERVERNUM
+    while [ -f /tmp/.X$i-lock ]; do
+        i=$(($i + 1))
+    done
+    echo $i
+}
+
+# Clean up files
+clean_up() {
+    if [ -e "$AUTHFILE" ]; then
+        XAUTHORITY=$AUTHFILE xauth remove ":$SERVERNUM" >>"$ERRORFILE" 2>&1
+    fi
+    if [ -n "$XVFB_RUN_TMPDIR" ]; then
+        if ! rm -r "$XVFB_RUN_TMPDIR"; then
+            error "problem while cleaning up temporary directory"
+            exit 5
+        fi
+    fi
+    if [ -n "$XVFBPID" ]; then
+        kill $XVFBPID
+    fi
+}
+
+# Parse the command line.
+ARGS=$(getopt --options +ae:f:hn:lp:s:w: \
+       --long auto-servernum,error-file:,auth-file:,help,server-num:,listen-tcp,xauth-protocol:,server-args:,wait: \
+       --name "$PROGNAME" -- "$@")
+GETOPT_STATUS=$?
+
+if [ $GETOPT_STATUS -ne 0 ]; then
+    error "internal error; getopt exited with status $GETOPT_STATUS"
+    exit 6
+fi
+
+eval set -- "$ARGS"
+
+while :; do
+    case "$1" in
+        -a|--auto-servernum) SERVERNUM=$(find_free_servernum); AUTONUM="yes" ;;
+        -e|--error-file) ERRORFILE="$2"; shift ;;
+        -f|--auth-file) AUTHFILE="$2"; shift ;;
+        -h|--help) SHOWHELP="yes" ;;
+        -n|--server-num) SERVERNUM="$2"; shift ;;
+        -l|--listen-tcp) LISTENTCP="" ;;
+        -p|--xauth-protocol) XAUTHPROTO="$2"; shift ;;
+        -s|--server-args) XVFBARGS="$2"; shift ;;
+        -w|--wait) STARTWAIT="$2"; shift ;;
+        --) shift; break ;;
+        *) error "internal error; getopt permitted \"$1\" unexpectedly"
+           exit 6
+           ;;
+    esac
+    shift
+done
+
+if [ "$SHOWHELP" ]; then
+    usage
+    exit 0
+fi
+
+if [ -z "$*" ]; then
+    usage "need a command to run" >&2
+    exit 2
+fi
+
+if ! which xauth >/dev/null; then
+    error "xauth command not found"
+    exit 3
+fi
+
+# tidy up after ourselves
+trap clean_up EXIT
+
+# If the user did not specify an X authorization file to use, set up a temporary
+# directory to house one.
+if [ -z "$AUTHFILE" ]; then
+    XVFB_RUN_TMPDIR="$(mktemp -d -t $PROGNAME.XXXXXX)"
+    # Create empty file to avoid xauth warning
+    AUTHFILE=$(tempfile -n "$XVFB_RUN_TMPDIR/Xauthority")
+fi
+
+# Start Xvfb.
+MCOOKIE=$(mcookie)
+tries=10
+while [ $tries -gt 0 ]; do
+    tries=$(( $tries - 1 ))
+    XAUTHORITY=$AUTHFILE xauth source - << EOF >>"$ERRORFILE" 2>&1
+add :$SERVERNUM $XAUTHPROTO $MCOOKIE
+EOF
+    XAUTHORITY=$AUTHFILE Xvfb ":$SERVERNUM" $XVFBARGS $LISTENTCP >>"$ERRORFILE" 2>&1 &
+    XVFBPID=$!
+
+    sleep "$STARTWAIT"
+    if kill -0 $XVFBPID 2>/dev/null; then
+        break
+    elif [ -n "$AUTONUM" ]; then
+        # The display is in use so try another one (if '-a' was specified).
+        SERVERNUM=$((SERVERNUM + 1))
+        SERVERNUM=$(find_free_servernum)
+        continue
+    fi
+    error "Xvfb failed to start" >&2
+    exit 1
+done
+
+# Start the command and save its exit status.
+set +e
+DISPLAY=:$SERVERNUM XAUTHORITY=$AUTHFILE "$@" 2>&1
+RETVAL=$?
+set -e
+
+# Return the executed command's exit status.
+exit $RETVAL
+
+# vim:set ai et sts=4 sw=4 tw=80:
--- a/devtools/devctl.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/devtools/devctl.py	Thu Mar 01 08:55:14 2012 +0100
@@ -32,7 +32,7 @@
 from logilab.common import STD_BLACKLIST
 
 from cubicweb.__pkginfo__ import version as cubicwebversion
-from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage
+from cubicweb import CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage, ExecutionError
 from cubicweb.cwctl import CWCTL
 from cubicweb.cwconfig import CubicWebNoAppConfiguration
 from cubicweb.toolsutils import (SKEL_EXCLUDE, Command, copy_skeleton,
@@ -303,7 +303,7 @@
         from logilab.common.shellutils import globfind, find, rm
         from logilab.common.modutils import get_module_files
         from cubicweb.i18n import extract_from_tal, execute
-        tempdir = tempfile.mkdtemp()
+        tempdir = tempfile.mkdtemp(prefix='cw-')
         cwi18ndir = WebConfiguration.i18n_lib_dir()
         print '-> extract schema messages.'
         schemapot = osp.join(tempdir, 'schema.pot')
@@ -377,7 +377,8 @@
                      for cube in DevConfiguration.available_cubes()]
             cubes = [cubepath for cubepath in cubes
                      if osp.exists(osp.join(cubepath, 'i18n'))]
-        update_cubes_catalogs(cubes)
+        if not update_cubes_catalogs(cubes):
+            raise ExecutionError("update cubes i18n catalog failed")
 
 
 def update_cubes_catalogs(cubes):
@@ -391,6 +392,7 @@
             import traceback
             traceback.print_exc()
             print '-> error while updating catalogs for cube', cubedir
+            return False
         else:
             # instructions pour la suite
             if toedit:
@@ -399,6 +401,7 @@
                 print '* ' + '\n* '.join(toedit)
                 print ('When you are done, run "cubicweb-ctl i18ninstance '
                        '<yourinstance>" to see changes in your instances.')
+            return True
 
 def update_cube_catalogs(cubedir):
     import shutil
--- a/devtools/qunit.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/devtools/qunit.py	Thu Mar 01 08:55:14 2012 +0100
@@ -64,7 +64,7 @@
 
     def __init__(self, url=None):
         self._process = None
-        self._tmp_dir = mkdtemp()
+        self._tmp_dir = mkdtemp(prefix='cwtest-ffxprof-')
         self._profile_data = {'uid': uuid4()}
         self._profile_name = self.profile_name_mask % self._profile_data
         fnull = open(os.devnull, 'w')
@@ -72,7 +72,7 @@
         stderr = TemporaryFile()
         self.firefox_cmd = ['firefox', '-no-remote']
         if os.name == 'posix':
-            self.firefox_cmd = ['xvfb-run', '-a'] + self.firefox_cmd
+            self.firefox_cmd = [osp.join(osp.dirname(__file__), 'data', 'xvfb-run.sh'), '-a'] + self.firefox_cmd
         try:
             home = osp.expanduser('~')
             user = getlogin()
--- a/ext/test/unittest_rest.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/ext/test/unittest_rest.py	Thu Mar 01 08:55:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -15,9 +15,6 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
 from logilab.common.testlib import unittest_main
 from cubicweb.devtools.testlib import CubicWebTC
 
@@ -60,23 +57,23 @@
     def test_rql_role_with_vid(self):
         context = self.context()
         out = rest_publish(context, ':rql:`Any X WHERE X is CWUser:table`')
-        self.assert_(out.endswith('<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a>'
-                                  '</td></tr></tbody></table></div>\n</div>\n</p>\n'))
+        self.assertTrue(out.endswith('<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a>'
+                                     '</td></tr>\n</tbody></table></div></p>\n'))
 
     def test_rql_role_with_vid_empty_rset(self):
         context = self.context()
         out = rest_publish(context, ':rql:`Any X WHERE X is CWUser, X login "nono":table`')
-        self.assert_(out.endswith('<p><div class="searchMessage"><strong>No result matching query</strong></div>\n</p>\n'))
+        self.assertTrue(out.endswith('<p><div class="searchMessage"><strong>No result matching query</strong></div>\n</p>\n'))
 
     def test_rql_role_with_unknown_vid(self):
         context = self.context()
         out = rest_publish(context, ':rql:`Any X WHERE X is CWUser:toto`')
-        self.assert_(out.startswith("<p>an error occured while interpreting this rql directive: ObjectNotFound(u'toto',)</p>"))
+        self.assertTrue(out.startswith("<p>an error occured while interpreting this rql directive: ObjectNotFound(u'toto',)</p>"))
 
     def test_rql_role_without_vid(self):
         context = self.context()
         out = rest_publish(context, ':rql:`Any X WHERE X is CWUser`')
-        self.assertEqual(out, u'<p><h1>cwuser_plural</h1><div class="section"><a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div><div class="section"><a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div></p>\n')
+        self.assertEqual(out, u'<p><h1>CWUser_plural</h1><div class="section"><a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div><div class="section"><a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div></p>\n')
 
 if __name__ == '__main__':
     unittest_main()
--- a/i18n/de.po	Thu Mar 01 08:51:32 2012 +0100
+++ b/i18n/de.po	Thu Mar 01 08:55:14 2012 +0100
@@ -962,6 +962,9 @@
 msgid "a float is expected"
 msgstr "Eine Dezimalzahl (float) wird erwartet."
 
+msgid "a number (in seconds) or 20s, 10min, 24h or 4d are expected"
+msgstr ""
+
 msgid ""
 "a simple cache entity characterized by a name and a validity date. The "
 "target application is responsible for updating timestamp when necessary to "
--- a/i18n/en.po	Thu Mar 01 08:51:32 2012 +0100
+++ b/i18n/en.po	Thu Mar 01 08:55:14 2012 +0100
@@ -922,6 +922,9 @@
 msgid "a float is expected"
 msgstr ""
 
+msgid "a number (in seconds) or 20s, 10min, 24h or 4d are expected"
+msgstr ""
+
 msgid ""
 "a simple cache entity characterized by a name and a validity date. The "
 "target application is responsible for updating timestamp when necessary to "
--- a/i18n/es.po	Thu Mar 01 08:51:32 2012 +0100
+++ b/i18n/es.po	Thu Mar 01 08:55:14 2012 +0100
@@ -967,6 +967,9 @@
 msgid "a float is expected"
 msgstr "un nĂºmero flotante es requerido"
 
+msgid "a number (in seconds) or 20s, 10min, 24h or 4d are expected"
+msgstr ""
+
 msgid ""
 "a simple cache entity characterized by a name and a validity date. The "
 "target application is responsible for updating timestamp when necessary to "
--- a/i18n/fr.po	Thu Mar 01 08:51:32 2012 +0100
+++ b/i18n/fr.po	Thu Mar 01 08:55:14 2012 +0100
@@ -967,6 +967,9 @@
 msgid "a float is expected"
 msgstr "un nombre flottant est attendu"
 
+msgid "a number (in seconds) or 20s, 10min, 24h or 4d are expected"
+msgstr "un nombre (en seconde) ou 20s, 10min, 24h ou 4d sont attendus"
+
 msgid ""
 "a simple cache entity characterized by a name and a validity date. The "
 "target application is responsible for updating timestamp when necessary to "
--- a/misc/migration/3.12.0_Any.py	Thu Mar 01 08:51:32 2012 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-if schema['TZDatetime'].eid is None:
-    add_entity_type('TZDatetime')
-if schema['TZTime'].eid is None:
-    add_entity_type('TZTime')
--- a/misc/migration/3.14.4_Any.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/misc/migration/3.14.4_Any.py	Thu Mar 01 08:55:14 2012 +0100
@@ -4,6 +4,7 @@
 rdefdef = schema['CWSource'].rdef('name')
 attrtype = y2sql.type_from_constraints(dbhelper, rdefdef.object, rdefdef.constraints).split()[0]
 
-sql(dbhelper.sql_change_col_type('entities', 'asource', attrtype, False))
-sql(dbhelper.sql_change_col_type('entities', 'source', attrtype, False))
-sql(dbhelper.sql_change_col_type('deleted_entities', 'source', attrtype, False))
+cursor = session.cnxset['system']
+dbhelper.change_col_type(cursor, 'entities', 'asource', attrtype, False)
+dbhelper.change_col_type(cursor, 'entities', 'source', attrtype, False)
+dbhelper.change_col_type(cursor, 'deleted_entities', 'source', attrtype, False)
--- a/misc/migration/bootstrapmigration_repository.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/misc/migration/bootstrapmigration_repository.py	Thu Mar 01 08:55:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -40,6 +40,13 @@
     sql('UPDATE entities SET asource=cw_name  '
         'FROM cw_CWSource, cw_source_relation '
         'WHERE entities.eid=cw_source_relation.eid_from AND cw_source_relation.eid_to=cw_CWSource.cw_eid')
+    commit()
+
+if schema['TZDatetime'].eid is None:
+    add_entity_type('TZDatetime', auto=False)
+if schema['TZTime'].eid is None:
+    add_entity_type('TZTime', auto=False)
+
 
 if applcubicwebversion <= (3, 14, 0) and cubicwebversion >= (3, 14, 0):
     if 'require_permission' in schema and not 'localperms'in repo.config.cubes():
--- a/rqlrewrite.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/rqlrewrite.py	Thu Mar 01 08:55:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -228,19 +228,19 @@
                                            if not r in sti['rhsrelations'])
                 else:
                     vi['rhs_rels'] = vi['lhs_rels'] = {}
-        parent = None
+        previous = None
         inserted = False
         for rqlexpr in rqlexprs:
             self.current_expr = rqlexpr
             if varexistsmap is None:
                 try:
-                    new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, parent)
+                    new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, previous)
                 except Unsupported:
                     continue
                 inserted = True
                 if new is not None and self._insert_scope is None:
                     self.exists_snippet[rqlexpr] = new
-                parent = parent or new
+                previous = previous or new
             else:
                 # called to reintroduce snippet due to ambiguity creation,
                 # so skip snippets which are not introducing this ambiguity
@@ -251,16 +251,21 @@
             # no rql expression found matching rql solutions. User has no access right
             raise Unauthorized() # XXX may also be because of bad constraints in schema definition
 
-    def insert_snippet(self, varmap, snippetrqlst, parent=None):
+    def insert_snippet(self, varmap, snippetrqlst, previous=None):
         new = snippetrqlst.where.accept(self)
         existing = self.existingvars
         self.existingvars = None
         try:
-            return self._insert_snippet(varmap, parent, new)
+            return self._insert_snippet(varmap, previous, new)
         finally:
             self.existingvars = existing
 
-    def _insert_snippet(self, varmap, parent, new):
+    def _insert_snippet(self, varmap, previous, new):
+        """insert `new` snippet into the syntax tree, which have been rewritten
+        using `varmap`. In cases where an action is protected by several rql
+        expresssion, `previous` will be the first rql expression which has been
+        inserted, and so should be ORed with the following expressions.
+        """
         if new is not None:
             if self._insert_scope is None:
                 insert_scope = None
@@ -274,28 +279,28 @@
                 insert_scope = self._insert_scope
             if self._insert_scope is None and any(vi.get('stinfo', {}).get('optrelations')
                                                   for vi in self.varinfos):
-                assert parent is None
-                self._insert_scope = self.snippet_subquery(varmap, new)
+                assert previous is None
+                self._insert_scope, new = self.snippet_subquery(varmap, new)
                 self.insert_pending()
                 #self._insert_scope = None
-                return
+                return new
             if not isinstance(new, (n.Exists, n.Not)):
                 new = n.Exists(new)
-            if parent is None:
+            if previous is None:
                 insert_scope.add_restriction(new)
             else:
-                grandpa = parent.parent
-                or_ = n.Or(parent, new)
-                grandpa.replace(parent, or_)
+                grandpa = previous.parent
+                or_ = n.Or(previous, new)
+                grandpa.replace(previous, or_)
             if not self.removing_ambiguity:
                 try:
                     self.compute_solutions()
                 except Unsupported:
                     # some solutions have been lost, can't apply this rql expr
-                    if parent is None:
+                    if previous is None:
                         self.current_statement().remove_node(new, undefine=True)
                     else:
-                        grandpa.replace(or_, parent)
+                        grandpa.replace(or_, previous)
                         self._cleanup_inserted(new)
                     raise
                 else:
@@ -419,7 +424,7 @@
             # some solutions have been lost, can't apply this rql expr
             self.select.remove_subquery(self.select.with_[-1])
             raise
-        return subselect
+        return subselect, snippetrqlst
 
     def remove_ambiguities(self, snippets, newsolutions):
         # the snippet has introduced some ambiguities, we have to resolve them
--- a/schema.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/schema.py	Thu Mar 01 08:55:14 2012 +0100
@@ -27,7 +27,7 @@
 
 from logilab.common.decorators import cached, clear_cache, monkeypatch
 from logilab.common.logging_ext import set_log_methods
-from logilab.common.deprecation import deprecated, class_moved
+from logilab.common.deprecation import deprecated, class_moved, moved
 from logilab.common.textutils import splitstrip
 from logilab.common.graph import get_cycles
 from logilab.common.compat import any
@@ -1250,7 +1250,10 @@
 from yams.buildobjs import RichString
 from yams.constraints import StaticVocabularyConstraint
 
-RichString = class_moved(RichString)
+try: # for yams < 0.35
+    RichString = class_moved(RichString)
+except TypeError:
+    RichString = moved('yams.buildobjs', 'RichString')
 
 StaticVocabularyConstraint = class_moved(StaticVocabularyConstraint)
 FormatConstraint = class_moved(FormatConstraint)
--- a/server/migractions.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/server/migractions.py	Thu Mar 01 08:55:14 2012 +0100
@@ -1112,7 +1112,7 @@
             schemaobj = getattr(rdef, attr)
             if getattr(schemaobj, 'eid', None) is None:
                 schemaobj.eid =  self.repo.schema[schemaobj].eid
-                assert schemaobj.eid is not None
+                assert schemaobj.eid is not None, schemaobj
         return rdef
 
     def cmd_drop_relation_definition(self, subjtype, rtype, objtype, commit=True):
--- a/server/sources/rql2sql.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/server/sources/rql2sql.py	Thu Mar 01 08:55:14 2012 +0100
@@ -1454,6 +1454,8 @@
         lhs, rhs = mexpr.get_parts()
         # check for string concatenation
         operator = mexpr.operator
+        if operator == '%':
+            operator = '%%'
         try:
             if mexpr.operator == '+' and mexpr.get_type(self._state.solution, self._args) == 'String':
                 return '(%s)' % self.dbhelper.sql_concat_string(lhs.accept(self),
--- a/server/test/unittest_rql2sql.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/server/test/unittest_rql2sql.py	Thu Mar 01 08:55:14 2012 +0100
@@ -1744,6 +1744,9 @@
 GROUP BY CAST(EXTRACT(YEAR from _X.cw_modification_date) AS INTEGER),CAST(EXTRACT(MONTH from _X.cw_modification_date) AS INTEGER)
 ORDER BY 1'''),
 
+    def test_modulo(self):
+        self._check('Any 5 % 2', '''SELECT (5 % 2)''')
+
 
 class SqlServer2005SQLGeneratorTC(PostgresSQLGeneratorTC):
     backend = 'sqlserver2005'
--- a/test/unittest_rqlrewrite.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/test/unittest_rqlrewrite.py	Thu Mar 01 08:55:14 2012 +0100
@@ -183,9 +183,9 @@
         self.assertEqual(rqlst.as_string(),
                          "Any A,C,T WHERE A documented_by C?, A is Affaire "
                          "WITH C,T BEING (Any C,T WHERE C title T, "
-                         "EXISTS(C in_state B, D in_group F, G require_state B, G name 'read', G require_group F), "
-                         "D eid %(A)s, C is Card, "
-                         "EXISTS(C in_state E, E name 'public'))")
+                         "(EXISTS(C in_state B, D in_group F, G require_state B, G name 'read', G require_group F)) "
+                         "OR (EXISTS(C in_state E, E name 'public')), "
+                         "D eid %(A)s, C is Card)")
 
     def test_optional_var_4(self):
         constraint1 = 'A created_by U, X documented_by A'
@@ -199,8 +199,8 @@
                              u'Any X,LA,Y WHERE LA? documented_by X, LA concerne Y, B eid %(C)s, '
                              'EXISTS(X created_by B), EXISTS(Y created_by B), '
                              'X is Card, Y is IN(Division, Note, Societe) '
-                             'WITH LA BEING (Any LA WHERE EXISTS(A created_by B, LA documented_by A), '
-                             'B eid %(D)s, LA is Affaire, EXISTS(E created_by B, LA concerne E))')
+                             'WITH LA BEING (Any LA WHERE (EXISTS(A created_by B, LA documented_by A)) OR (EXISTS(E created_by B, LA concerne E)), '
+                             'B eid %(D)s, LA is Affaire)')
 
     def test_optional_var_inlined(self):
         c1 = ('X require_permission P')
@@ -431,6 +431,18 @@
         self.assertEqual(rqlst.as_string(),
                          u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire')
 
+    def test_rqlexpr_multiexpr_outerjoin(self):
+        c1 = RRQLExpression('X owned_by Z, Z login "hop"', 'X')
+        c2 = RRQLExpression('X owned_by Z, Z login "hip"', 'X')
+        c3 = RRQLExpression('X owned_by Z, Z login "momo"', 'X')
+        rqlst = rqlhelper.parse('Any A WHERE A documented_by C?', annotate=False)
+        rewrite(rqlst, {('C', 'X'): (c1, c2, c3)}, {}, 'X')
+        self.assertEqual(rqlst.as_string(),
+                         u'Any A WHERE A documented_by C?, A is Affaire '
+                         'WITH C BEING (Any C WHERE ((EXISTS(C owned_by B, B login "hop")) '
+                         'OR (EXISTS(C owned_by D, D login "momo"))) '
+                         'OR (EXISTS(C owned_by A, A login "hip")), C is Card)')
+
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/controller.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/web/controller.py	Thu Mar 01 08:55:14 2012 +0100
@@ -207,10 +207,12 @@
         if '__redirectpath' in self._cw.form:
             # if redirect path was explicitly specified in the form, use it
             path = self._cw.form['__redirectpath']
-            url = self._cw.build_url(path, **newparams)
+            url = self._cw.build_url(path)
             url = append_url_params(url, self._cw.form.get('__redirectparams'))
         else:
             url = self._cw.last_visited_page()
+        # The newparams must update the params in all cases
+        url = self._cw.rebuild_url(url, **newparams)
         raise Redirect(url)
 
 
--- a/web/formfields.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/web/formfields.py	Thu Mar 01 08:55:14 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -43,6 +43,7 @@
 .. autoclass:: cubicweb.web.formfields.DateField()
 .. autoclass:: cubicweb.web.formfields.DateTimeField()
 .. autoclass:: cubicweb.web.formfields.TimeField()
+.. autoclass:: cubicweb.web.formfields.TimeIntervalField()
 
 Compound fields
 ''''''''''''''''
@@ -63,11 +64,13 @@
 __docformat__ = "restructuredtext en"
 
 from warnings import warn
-from datetime import datetime
+from datetime import datetime, timedelta
 
 from logilab.mtconverter import xml_escape
 from logilab.common import nullobject
 from logilab.common.date import ustrftime
+from logilab.common.configuration import format_time
+from logilab.common.textutils import apply_units, TIME_UNITS
 
 from yams.schema import KNOWN_METAATTRIBUTES, role_name
 from yams.constraints import (SizeConstraint, StaticVocabularyConstraint,
@@ -929,6 +932,38 @@
         return None
 
 
+class TimeIntervalField(StringField):
+    """Use this field to edit time interval (`Interval` yams type).
+
+    Unless explicitly specified, the widget for this field will be a
+    :class:`~cubicweb.web.formwidgets.TextInput`.
+    """
+    widget = fw.TextInput
+
+    def format_single_value(self, req, value):
+        if value:
+            value = format_time(value.days * 24 * 3600 + value.seconds)
+            return unicode(value)
+        return u''
+
+    def example_format(self, req):
+        """return a sample string describing what can be given as input for this
+        field
+        """
+        return u'20s, 10min, 24h, 4d'
+
+    def _ensure_correctly_typed(self, form, value):
+        if isinstance(value, basestring):
+            value = value.strip()
+            if not value:
+                return None
+            try:
+                value = apply_units(value, TIME_UNITS)
+            except ValueError:
+                raise ProcessFormError(form._cw._('a number (in seconds) or 20s, 10min, 24h or 4d are expected'))
+        return timedelta(0, value)
+
+
 class DateField(StringField):
     """Use this field to edit date (`Date` yams type).
 
@@ -1201,5 +1236,5 @@
     'TZDatetime': DateTimeField,
     'Time':       TimeField,
     'TZTime':     TimeField,
-    # XXX implement 'Interval': TimeIntervalField,
+    'Interval':   TimeIntervalField,
     }
--- a/web/test/unittest_views_basetemplates.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/web/test/unittest_views_basetemplates.py	Thu Mar 01 08:55:14 2012 +0100
@@ -35,6 +35,13 @@
         self.set_option('allow-email-login', 'no')
         self.assertEqual(self._login_labels(), ['login', 'password'])
 
+
+class MainNoTopTemplateTC(CubicWebTC):
+
+    def test_valid_xhtml(self):
+        self.view('index', template='main-no-top')
+
+
 if __name__ == '__main__':
     from logilab.common.testlib import unittest_main
     unittest_main()
--- a/web/views/basetemplates.py	Thu Mar 01 08:51:32 2012 +0100
+++ b/web/views/basetemplates.py	Thu Mar 01 08:55:14 2012 +0100
@@ -256,10 +256,10 @@
         whead(u'\n'.join(additional_headers) + u'\n')
         self.wview('htmlheader', rset=self.cw_rset)
         w = self.w
-        w(u'<title>%s</title>\n' % xml_escape(page_title))
+        whead(u'<title>%s</title>\n' % xml_escape(page_title))
         w(u'<body>\n')
         w(u'<div id="page">')
-        w(u'<table width="100%" height="100%" border="0"><tr>\n')
+        w(u'<table width="100%" border="0" id="mainLayout"><tr>\n')
         w(u'<td id="navColumnLeft">\n')
         self.topleft_header()
         boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
@@ -270,11 +270,7 @@
                 box.render(w=w)
             self.w(u'</div>\n')
         w(u'</td>')
-        w(u'<td id="contentcol" rowspan="2">')
-        w(u'<div id="pageContent">\n')
-        vtitle = self._cw.form.get('vtitle')
-        if vtitle:
-            w(u'<div class="vtitle">%s</div>' % xml_escape(vtitle))
+        w(u'<td id="contentColumn" rowspan="2">')
 
     def topleft_header(self):
         logo = self._cw.vreg['components'].select_or_none('logo', self._cw,