--- 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,