merge from 3.19 branch
authorJulien Cristau <julien.cristau@logilab.fr>
Fri, 27 Jun 2014 11:48:26 +0200
changeset 9821 2077c8da1893
parent 9819 95902c0b991b (diff)
parent 9817 bb719d857421 (current diff)
child 9822 4a118bfd6ab4
merge from 3.19 branch
req.py
server/sources/native.py
view.py
web/data/cubicweb.ajax.js
web/data/cubicweb.htmlhelpers.js
web/data/cubicweb.js
web/data/cubicweb.old.css
web/formfields.py
web/formwidgets.py
web/request.py
wsgi/request.py
--- a/migration.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/migration.py	Fri Jun 27 11:48:26 2014 +0200
@@ -247,12 +247,13 @@
         local_ctx = self._create_context()
         try:
             import readline
-            from rlcompleter import Completer
+            from cubicweb.toolsutils import CWShellCompleter
         except ImportError:
             # readline not available
             pass
         else:
-            readline.set_completer(Completer(local_ctx).complete)
+            rql_completer = CWShellCompleter(local_ctx)
+            readline.set_completer(rql_completer.complete)
             readline.parse_and_bind('tab: complete')
             home_key = 'HOME'
             if sys.platform == 'win32':
--- a/req.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/req.py	Fri Jun 27 11:48:26 2014 +0200
@@ -485,12 +485,16 @@
             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
                              % {'value': value, 'format': format})
 
+    def _base_url(self, secure=None):
+        if secure:
+            return self.vreg.config.get('https-url') or self.vreg.config['base-url']
+        return self.vreg.config['base-url']
+
     def base_url(self, secure=None):
         """return the root url of the instance
         """
-        if secure:
-            return self.vreg.config.get('https-url') or self.vreg.config['base-url']
-        return self.vreg.config['base-url']
+        url = self._base_url(secure=secure)
+        return url if url is None else url.rstrip('/') + '/'
 
     # abstract methods to override according to the web front-end #############
 
--- a/server/sources/native.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/server/sources/native.py	Fri Jun 27 11:48:26 2014 +0200
@@ -323,10 +323,16 @@
                   'want trusted authentication for the database connection',
           'group': 'native-source', 'level': 2,
           }),
+        ('db-statement-timeout',
+         {'type': 'int',
+          'default': 0,
+          'help': 'sql statement timeout, in milliseconds (postgres only)',
+          'group': 'native-source', 'level': 2,
+          }),
     )
 
     def __init__(self, repo, source_config, *args, **kwargs):
-        SQLAdapterMixIn.__init__(self, source_config)
+        SQLAdapterMixIn.__init__(self, source_config, repairing=repo.config.repairing)
         self.authentifiers = [LoginPasswordAuthentifier(self)]
         if repo.config['allow-email-login']:
             self.authentifiers.insert(0, EmailPasswordAuthentifier(self))
--- a/server/sqlutils.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/server/sqlutils.py	Fri Jun 27 11:48:26 2014 +0200
@@ -299,7 +299,7 @@
     """
     cnx_wrap = ConnectionWrapper
 
-    def __init__(self, source_config):
+    def __init__(self, source_config, repairing=False):
         try:
             self.dbdriver = source_config['db-driver'].lower()
             dbname = source_config['db-name']
@@ -328,6 +328,14 @@
         if self.dbdriver == 'sqlite':
             self.cnx_wrap = SqliteConnectionWrapper
             self.dbhelper.dbname = abspath(self.dbhelper.dbname)
+        if not repairing:
+            statement_timeout = int(source_config.get('db-statement-timeout', 0))
+            if statement_timeout > 0:
+                def set_postgres_timeout(cnx):
+                    cnx.cursor().execute('SET statement_timeout to %d' % statement_timeout)
+                    cnx.commit()
+                postgres_hooks = SQL_CONNECT_HOOKS['postgres']
+                postgres_hooks.append(set_postgres_timeout)
 
     def wrapped_connection(self):
         """open and return a connection to the database, wrapped into a class
--- a/test/unittest_dbapi.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/test/unittest_dbapi.py	Fri Jun 27 11:48:26 2014 +0200
@@ -78,7 +78,7 @@
         with tempattr(cnx.vreg, 'config', config):
             cnx.use_web_compatible_requests('http://perdu.com')
             req = cnx.request()
-            self.assertEqual(req.base_url(), 'http://perdu.com')
+            self.assertEqual(req.base_url(), 'http://perdu.com/')
             self.assertEqual(req.from_controller(), 'view')
             self.assertEqual(req.relative_path(), '')
             req.ajax_replace_url('domid') # don't crash
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_toolsutils.py	Fri Jun 27 11:48:26 2014 +0200
@@ -0,0 +1,57 @@
+# copyright 2014 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/>.
+
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.toolsutils import RQLExecuteMatcher
+
+
+class RQLExecuteMatcherTests(TestCase):
+    def matched_query(self, text):
+        match = RQLExecuteMatcher.match(text)
+        if match is None:
+            return None
+        return match['rql_query']
+
+    def test_unknown_function_dont_match(self):
+        self.assertIsNone(self.matched_query('foo'))
+        self.assertIsNone(self.matched_query('rql('))
+        self.assertIsNone(self.matched_query('hell("")'))
+        self.assertIsNone(self.matched_query('eval("rql(\'bla\''))
+
+    def test_rql_other_parameters_dont_match(self):
+        self.assertIsNone(self.matched_query('rql("Any X WHERE X eid %(x)s")'))
+        self.assertIsNone(self.matched_query('rql("Any X WHERE X eid %(x)s", {'))
+        self.assertIsNone(self.matched_query('session.execute("Any X WHERE X eid %(x)s")'))
+        self.assertIsNone(self.matched_query('session.execute("Any X WHERE X eid %(x)s", {'))
+
+    def test_rql_function_match(self):
+        for func_expr in ('rql', 'session.execute'):
+            query = self.matched_query('%s("Any X WHERE X is ' % func_expr)
+            self.assertEqual(query, 'Any X WHERE X is ')
+
+    def test_offseted_rql_function_match(self):
+        """check indentation is allowed"""
+        for func_expr in ('  rql', '  session.execute'):
+            query = self.matched_query('%s("Any X WHERE X is ' % func_expr)
+            self.assertEqual(query, 'Any X WHERE X is ')
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/toolsutils.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/toolsutils.py	Fri Jun 27 11:48:26 2014 +0200
@@ -25,7 +25,12 @@
 import subprocess
 from os import listdir, makedirs, environ, chmod, walk, remove
 from os.path import exists, join, abspath, normpath
-
+import re
+from rlcompleter import Completer
+try:
+    import readline
+except ImportError: # readline not available, no completion
+    pass
 try:
     from os import symlink
 except ImportError:
@@ -264,3 +269,155 @@
         password = getpass('password: ')
     return connect(login=user, password=password, host=optconfig.host, database=appid)
 
+
+## cwshell helpers #############################################################
+
+class AbstractMatcher(object):
+    """Abstract class for CWShellCompleter's matchers.
+
+    A matcher should implement a ``possible_matches`` method. This
+    method has to return the list of possible completions for user's input.
+    Because of the python / readline interaction, each completion should
+    be a superset of the user's input.
+
+    NOTE: readline tokenizes user's input and only passes last token to
+    completers.
+    """
+
+    def possible_matches(self, text):
+        """return possible completions for user's input.
+
+        Parameters:
+            text: the user's input
+
+        Return:
+            a list of completions. Each completion includes the original input.
+        """
+        raise NotImplementedError()
+
+
+class RQLExecuteMatcher(AbstractMatcher):
+    """Custom matcher for rql queries.
+
+    If user's input starts with ``rql(`` or ``session.execute(`` and
+    the corresponding rql query is incomplete, suggest some valid completions.
+    """
+    query_match_rgx = re.compile(
+        r'(?P<func_prefix>\s*(?:rql)'  # match rql, possibly indented
+        r'|'                           # or
+        r'\s*(?:\w+\.execute))'        # match .execute, possibly indented
+        # end of <func_prefix>
+        r'\('                          # followed by a parenthesis
+        r'(?P<quote_delim>["\'])'      # a quote or double quote
+        r'(?P<parameters>.*)')         # and some content
+
+    def __init__(self, local_ctx, req):
+        self.local_ctx = local_ctx
+        self.req = req
+        self.schema = req.vreg.schema
+        self.rsb = req.vreg['components'].select('rql.suggestions', req)
+
+    @staticmethod
+    def match(text):
+        """check if ``text`` looks like a call to ``rql`` or ``session.execute``
+
+        Parameters:
+            text: the user's input
+
+        Returns:
+            None if it doesn't match, the query structure otherwise.
+        """
+        query_match = RQLExecuteMatcher.query_match_rgx.match(text)
+        if query_match is None:
+            return None
+        parameters_text = query_match.group('parameters')
+        quote_delim = query_match.group('quote_delim')
+        # first parameter is fully specified, no completion needed
+        if re.match(r"(.*?)%s" % quote_delim, parameters_text) is not None:
+            return None
+        func_prefix = query_match.group('func_prefix')
+        return {
+            # user's input
+            'text': text,
+            # rql( or session.execute(
+            'func_prefix': func_prefix,
+            # offset of rql query
+            'rql_offset': len(func_prefix) + 2,
+            # incomplete rql query
+            'rql_query': parameters_text,
+            }
+
+    def possible_matches(self, text):
+        """call ``rql.suggestions`` component to complete user's input.
+        """
+        # readline will only send last token, but we need the entire user's input
+        user_input = readline.get_line_buffer()
+        query_struct = self.match(user_input)
+        if query_struct is None:
+            return []
+        else:
+            # we must only send completions of the last token => compute where it
+            # starts relatively to the rql query itself.
+            completion_offset = readline.get_begidx() - query_struct['rql_offset']
+            rql_query = query_struct['rql_query']
+            return [suggestion[completion_offset:]
+                    for suggestion in self.rsb.build_suggestions(rql_query)]
+
+
+class DefaultMatcher(AbstractMatcher):
+    """Default matcher: delegate to standard's `rlcompleter.Completer`` class
+    """
+    def __init__(self, local_ctx):
+        self.completer = Completer(local_ctx)
+
+    def possible_matches(self, text):
+        if "." in text:
+            return self.completer.attr_matches(text)
+        else:
+            return self.completer.global_matches(text)
+
+
+class CWShellCompleter(object):
+    """Custom auto-completion helper for cubicweb-ctl shell.
+
+    ``CWShellCompleter`` provides a ``complete`` method suitable for
+    ``readline.set_completer``.
+
+    Attributes:
+        matchers: the list of ``AbstractMatcher`` instances that will suggest
+                  possible completions
+
+    The completion process is the following:
+
+    - readline calls the ``complete`` method with user's input,
+    - the ``complete`` method asks for each known matchers if
+      it can suggest completions for user's input.
+    """
+
+    def __init__(self, local_ctx):
+        # list of matchers to ask for possible matches on completion
+        self.matchers = [DefaultMatcher(local_ctx)]
+        self.matchers.insert(0, RQLExecuteMatcher(local_ctx, local_ctx['session']))
+
+    def complete(self, text, state):
+        """readline's completer method
+
+        cf http://docs.python.org/2/library/readline.html#readline.set_completer
+        for more details.
+
+        Implementation inspired by `rlcompleter.Completer`
+        """
+        if state == 0:
+            # reset self.matches
+            self.matches = []
+            for matcher in self.matchers:
+                matches = matcher.possible_matches(text)
+                if matches:
+                    self.matches = matches
+                    break
+            else:
+                return None # no matcher able to handle `text`
+        try:
+            return self.matches[state]
+        except IndexError:
+            return None
--- a/view.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/view.py	Fri Jun 27 11:48:26 2014 +0200
@@ -501,28 +501,6 @@
 class ReloadableMixIn(object):
     """simple mixin for reloadable parts of UI"""
 
-    def user_callback(self, cb, args, msg=None, nonify=False):
-        """register the given user callback and return a URL to call it ready to be
-        inserted in html
-        """
-        self._cw.add_js('cubicweb.ajax.js')
-        if nonify:
-            _cb = cb
-            def cb(*args):
-                _cb(*args)
-        cbname = self._cw.register_onetime_callback(cb, *args)
-        return self.build_js(cbname, xml_escape(msg or ''))
-
-    def build_update_js_call(self, cbname, msg):
-        rql = self.cw_rset.printable_rql()
-        return "javascript: %s" % js.userCallbackThenUpdateUI(
-            cbname, self.__regid__, rql, msg, self.__registry__, self.domid)
-
-    def build_reload_js_call(self, cbname, msg):
-        return "javascript: %s" % js.userCallbackThenReloadPage(cbname, msg)
-
-    build_js = build_update_js_call # expect updatable component by default
-
     @property
     def domid(self):
         return domid(self.__regid__)
--- a/web/application.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/application.py	Fri Jun 27 11:48:26 2014 +0200
@@ -23,6 +23,7 @@
 from time import clock, time
 from contextlib import contextmanager
 from warnings import warn
+import json
 
 import httplib
 
@@ -581,8 +582,10 @@
         status = httplib.INTERNAL_SERVER_ERROR
         if isinstance(ex, PublishException) and ex.status is not None:
             status = ex.status
-        req.status_out = status
-        json_dumper = getattr(ex, 'dumps', lambda : unicode(ex))
+        if req.status_out < 400:
+            # don't overwrite it if it's already set
+            req.status_out = status
+        json_dumper = getattr(ex, 'dumps', lambda : json.dumps({'reason': unicode(ex)}))
         return json_dumper()
 
     # special case handling
--- a/web/data/cubicweb.ajax.js	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/data/cubicweb.ajax.js	Fri Jun 27 11:48:26 2014 +0200
@@ -88,8 +88,8 @@
 });
 
 var AJAX_PREFIX_URL = 'ajax';
-var JSON_BASE_URL = baseuri() + 'json?';
-var AJAX_BASE_URL = baseuri() + AJAX_PREFIX_URL + '?';
+var JSON_BASE_URL = BASE_URL + 'json?';
+var AJAX_BASE_URL = BASE_URL + AJAX_PREFIX_URL + '?';
 
 
 jQuery.extend(cw.ajax, {
@@ -122,9 +122,7 @@
      * (e.g. http://..../data??resource1.js,resource2.js)
      */
     _modconcatLikeUrl: function(url) {
-        var base = baseuri();
-        if (!base.endswith('/')) { base += '/'; }
-        var modconcat_rgx = new RegExp('(' + base + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
+        var modconcat_rgx = new RegExp('(' + BASE_URL + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
         return modconcat_rgx.exec(url);
     },
 
@@ -379,8 +377,8 @@
  * dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
  */
 function loadRemote(url, form, reqtype, sync) {
-    if (!url.toLowerCase().startswith(baseuri().toLowerCase())) {
-        url = baseuri() + url;
+    if (!url.toLowerCase().startswith(BASE_URL.toLowerCase())) {
+        url = BASE_URL + url;
     }
     if (!sync) {
         var deferred = new Deferred();
@@ -601,7 +599,7 @@
                 var fck = new FCKeditor(this.id);
                 fck.Config['CustomConfigurationsPath'] = fckconfigpath;
                 fck.Config['DefaultLanguage'] = fcklang;
-                fck.BasePath = baseuri() + "fckeditor/";
+                fck.BasePath = BASE_URL + "fckeditor/";
                 fck.ReplaceTextarea();
             } else {
                 cw.log('fckeditor could not be found.');
--- a/web/data/cubicweb.css	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/data/cubicweb.css	Fri Jun 27 11:48:26 2014 +0200
@@ -205,10 +205,6 @@
   padding: 0px 0px 1px 1px;
 }
 
-li.invisible div{
-  display: inline;
-}
-
 .caption {
     font-weight: bold;
 }
--- a/web/data/cubicweb.edition.js	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/data/cubicweb.edition.js	Fri Jun 27 11:48:26 2014 +0200
@@ -67,7 +67,7 @@
                 rql: rql_for_eid(eid),
                 '__notemplate': 1
             };
-            var d = jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
+            var d = jQuery('#unrelatedDivs_' + eid).loadxhtml(BASE_URL + 'view', args, 'post', 'append');
             d.addCallback(function() {
                 _showMatchingSelect(eid, jQuery('#' + divId));
             });
--- a/web/data/cubicweb.facets.js	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/data/cubicweb.facets.js	Fri Jun 27 11:48:26 2014 +0200
@@ -69,7 +69,7 @@
         }
         var $focusLink = $('#focusLink');
         if ($focusLink.length) {
-            var url = baseuri()+ 'view?rql=' + encodeURIComponent(rql);
+            var url = BASE_URL + 'view?rql=' + encodeURIComponent(rql);
             if (vid) {
                 url += '&vid=' + encodeURIComponent(vid);
             }
--- a/web/data/cubicweb.htmlhelpers.js	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/data/cubicweb.htmlhelpers.js	Fri Jun 27 11:48:26 2014 +0200
@@ -12,20 +12,13 @@
 /**
  * .. function:: baseuri()
  *
- * returns the document's baseURI. (baseuri() uses document.baseURI if
- * available and inspects the <base> tag manually otherwise.)
+ * returns the document's baseURI.
  */
-function baseuri() {
-    if (typeof BASE_URL === 'undefined') {
-        // backward compatibility, BASE_URL might be undefined
-        var uri = document.baseURI;
-        if (uri) { // some browsers don't define baseURI
-            return uri.toLowerCase();
-        }
-        return jQuery('base').attr('href').toLowerCase();
-    }
-    return BASE_URL;
-}
+baseuri = cw.utils.deprecatedFunction(
+    "[3.20] baseuri() is deprecated, use BASE_URL instead",
+    function () {
+        return BASE_URL;
+    });
 
 /**
  * .. function:: setProgressCursor()
@@ -107,18 +100,6 @@
 }
 
 /**
- * .. function:: popupLoginBox()
- *
- * toggles visibility of login popup div
- */
-// XXX used exactly ONCE in basecomponents
-popupLoginBox = cw.utils.deprecatedFunction(
-    function() {
-        $('#popupLoginBox').toggleClass('hidden');
-        jQuery('#__login:visible').focus();
-});
-
-/**
  * .. function getElementsMatching(tagName, properties, \/* optional \*\/ parent)
  *
  * returns the list of elements in the document matching the tag name
--- a/web/data/cubicweb.js	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/data/cubicweb.js	Fri Jun 27 11:48:26 2014 +0200
@@ -208,91 +208,40 @@
     },
 
     /**
-     * .. function:: formContents(elem \/* = document.body *\/)
+     * .. function:: formContents(elem)
      *
-     * this implementation comes from MochiKit
+     * cannot use jQuery.serializeArray() directly because of FCKeditor
      */
-    formContents: function (elem /* = document.body */ ) {
-        var names = [];
-        var values = [];
-        if (typeof(elem) == "undefined" || elem === null) {
-            elem = document.body;
-        } else {
-            elem = cw.getNode(elem);
-        }
-        cw.utils.nodeWalkDepthFirst(elem, function (elem) {
-            var name = elem.name;
-            if (name && name.length) {
-                if (elem.disabled) {
-                    return null;
-                }
-                var tagName = elem.tagName.toUpperCase();
-                if (tagName === "INPUT" && (elem.type == "radio" || elem.type == "checkbox") && !elem.checked) {
-                    return null;
-                }
-                if (tagName === "SELECT") {
-                    if (elem.type == "select-one") {
-                        if (elem.selectedIndex >= 0) {
-                            var opt = elem.options[elem.selectedIndex];
-                            var v = opt.value;
-                            if (!v) {
-                                var h = opt.outerHTML;
-                                // internet explorer sure does suck.
-                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
-                                    v = opt.text;
-                                }
-                            }
-                            names.push(name);
-                            values.push(v);
+    formContents: function (elem) {
+        var $elem, array, names, values;
+        $elem = cw.jqNode(elem);
+        array = $elem.serializeArray();
+
+        if (typeof FCKeditor !== 'undefined') {
+            $elem.find('textarea').each(function (idx, textarea) {
+                var fck = FCKeditorAPI.GetInstance(textarea.id);
+                if (fck) {
+                    array = jQuery.map(array, function (dict) {
+                        if (dict.name === textarea.name) {
+                            // filter out the textarea's - likely empty - value ...
                             return null;
                         }
-                        // no form elements?
-                        names.push(name);
-                        values.push("");
-                        return null;
-                    } else {
-                        var opts = elem.options;
-                        if (!opts.length) {
-                            names.push(name);
-                            values.push("");
-                            return null;
-                        }
-                        for (var i = 0; i < opts.length; i++) {
-                            var opt = opts[i];
-                            if (!opt.selected) {
-                                continue;
-                            }
-                            var v = opt.value;
-                            if (!v) {
-                                var h = opt.outerHTML;
-                                // internet explorer sure does suck.
-                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
-                                    v = opt.text;
-                                }
-                            }
-                            names.push(name);
-                            values.push(v);
-                        }
-                        return null;
-                    }
+                        return dict;
+                    });
+                    // ... so we can put the HTML coming from FCKeditor instead.
+                    array.push({
+                        name: textarea.name,
+                        value: fck.GetHTML()
+                    });
                 }
-                if (tagName === "FORM" || tagName === "P" || tagName === "SPAN" || tagName === "DIV") {
-                    return elem.childNodes;
-                }
-		var value = elem.value;
-		if (tagName === "TEXTAREA") {
-		    if (typeof(FCKeditor) != 'undefined') {
-			var fck = FCKeditorAPI.GetInstance(elem.id);
-			if (fck) {
-			    value = fck.GetHTML();
-			}
-		    }
-		}
-                names.push(name);
-                values.push(value || '');
-                return null;
-            }
-            return elem.childNodes;
+            });
+        }
+
+        names = [];
+        values = [];
+        jQuery.each(array, function (idx, dict) {
+            names.push(dict.name);
+            values.push(dict.value);
         });
         return [names, values];
     },
--- a/web/data/cubicweb.old.css	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/data/cubicweb.old.css	Fri Jun 27 11:48:26 2014 +0200
@@ -225,10 +225,6 @@
   padding: 0px 0px 1px 1px;
 }
 
-li.invisible div {
-  display: inline;
-}
-
 .caption {
   font-weight: bold;
 }
--- a/web/data/cubicweb.timeline-bundle.js	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/data/cubicweb.timeline-bundle.js	Fri Jun 27 11:48:26 2014 +0200
@@ -3,8 +3,8 @@
  *  :organization: Logilab
  */
 
-var SimileAjax_urlPrefix = baseuri() + 'data/';
-var Timeline_urlPrefix = baseuri() + 'data/';
+var SimileAjax_urlPrefix = BASE_URL + 'data/';
+var Timeline_urlPrefix = BASE_URL + 'data/';
 
 /*
  *  Simile Ajax API
--- a/web/formfields.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/formfields.py	Fri Jun 27 11:48:26 2014 +0200
@@ -529,6 +529,7 @@
     """
     widget = fw.TextArea
     size = 45
+    placeholder = None
 
     def __init__(self, name=None, max_length=None, **kwargs):
         self.max_length = max_length # must be set before super call
@@ -547,6 +548,9 @@
         elif isinstance(self.widget, fw.TextInput):
             self.init_text_input(self.widget)
 
+        if self.placeholder:
+            self.widget.attrs.setdefault('placeholder', self.placeholder)
+
     def init_text_input(self, widget):
         if self.max_length:
             widget.attrs.setdefault('size', min(self.size, self.max_length))
@@ -557,6 +561,11 @@
             widget.attrs.setdefault('cols', 60)
             widget.attrs.setdefault('rows', 5)
 
+    def set_placeholder(self, placeholder):
+        self.placeholder = placeholder
+        if self.widget and self.placeholder:
+            self.widget.attrs.setdefault('placeholder', self.placeholder)
+
 
 class PasswordField(StringField):
     """Use this field to edit password (`Password` yams type, encoded python
--- a/web/formwidgets.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/formwidgets.py	Fri Jun 27 11:48:26 2014 +0200
@@ -210,6 +210,8 @@
             attrs['id'] = field.dom_id(form, self.suffix)
         if self.settabindex and not 'tabindex' in attrs:
             attrs['tabindex'] = form._cw.next_tabindex()
+        if 'placeholder' in attrs:
+            attrs['placeholder'] = form._cw._(attrs['placeholder'])
         return attrs
 
     def values(self, form, field):
--- a/web/http_headers.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/http_headers.py	Fri Jun 27 11:48:26 2014 +0200
@@ -1324,6 +1324,9 @@
         h = self._headers.get(name, None)
         r = self.handler.generate(name, h)
         if r is not None:
+            assert isinstance(r, list)
+            for v in r:
+                assert isinstance(v, str)
             self._raw_headers[name] = r
         return r
 
@@ -1362,6 +1365,9 @@
         Value should be a list of strings, each being one header of the
         given name.
         """
+        assert isinstance(value, list)
+        for v in value:
+            assert isinstance(v, str)
         name = name.lower()
         self._raw_headers[name] = value
         self._headers[name] = _RecalcNeeded
--- a/web/request.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/request.py	Fri Jun 27 11:48:26 2014 +0200
@@ -162,7 +162,7 @@
         self.ajax_request = value
     json_request = property(_get_json_request, _set_json_request)
 
-    def base_url(self, secure=None):
+    def _base_url(self, secure=None):
         """return the root url of the instance
 
         secure = False -> base-url
@@ -175,7 +175,7 @@
         if secure:
             base_url = self.vreg.config.get('https-url')
         if base_url is None:
-            base_url = super(_CubicWebRequestBase, self).base_url()
+            base_url = super(_CubicWebRequestBase, self)._base_url()
         return base_url
 
     @property
@@ -769,10 +769,6 @@
             if 'Expires' not in self.headers_out:
                 # Expires header seems to be required by IE7 -- Are you sure ?
                 self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-            if self.http_method() == 'HEAD':
-                self.status_out = 200
-                # XXX replace by True once validate_cache bw compat method is dropped
-                return 200
             # /!\ no raise, the function returns and we keep processing the request
         else:
             # overwrite headers_out to forge a brand new not-modified response
--- a/web/test/unittest_http.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/test/unittest_http.py	Fri Jun 27 11:48:26 2014 +0200
@@ -227,7 +227,7 @@
         hout = [('etag', 'rhino/really-not-babar'),
                ]
         req = _test_cache(hin, hout, method='HEAD')
-        self.assertCache(200, req.status_out, 'modifier HEAD verb')
+        self.assertCache(None, req.status_out, 'modifier HEAD verb')
         # not modified
         hin  = [('if-none-match', 'babar'),
                ]
--- a/web/test/unittest_web.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/test/unittest_web.py	Fri Jun 27 11:48:26 2014 +0200
@@ -102,6 +102,18 @@
         webreq = self.web_request(headers=headers)
         self.assertIn('lang="en"', webreq.read())
 
+    def test_response_codes(self):
+        with self.admin_access.client_cnx() as cnx:
+            admin_eid = cnx.user.eid
+        # guest can't see admin
+        webreq = self.web_request('/%d' % admin_eid)
+        self.assertEqual(webreq.status, 403)
+
+        # but admin can
+        self.web_login()
+        webreq = self.web_request('/%d' % admin_eid)
+        self.assertEqual(webreq.status, 200)
+
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/views/ajaxedit.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/views/ajaxedit.py	Fri Jun 27 11:48:26 2014 +0200
@@ -36,8 +36,6 @@
     cw_property_defs = {} # don't want to inherit this from Box
     expected_kwargs = form_params = ('rtype', 'target')
 
-    build_js = component.EditRelationMixIn.build_reload_js_call
-
     def cell_call(self, row, col, rtype=None, target=None, etype=None):
         self.rtype = rtype or self._cw.form['rtype']
         self.target = target or self._cw.form['target']
--- a/web/views/autoform.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/web/views/autoform.py	Fri Jun 27 11:48:26 2014 +0200
@@ -506,7 +506,7 @@
                 w(u'<td>')
                 w(u'<ul>')
                 for viewparams in related:
-                    w(u'<li class="invisible">%s<div id="span%s" class="%s">%s</div></li>'
+                    w(u'<li class="invisible">%s<span id="span%s" class="%s">%s</span></li>'
                       % (viewparams[1], viewparams[0], viewparams[2], viewparams[3]))
                 if not form.force_display and form.maxrelitems < len(related):
                     link = (u'<span class="invisible">'
--- a/wsgi/request.py	Wed Jun 11 10:16:13 2014 +0200
+++ b/wsgi/request.py	Fri Jun 27 11:48:26 2014 +0200
@@ -59,7 +59,7 @@
 
         headers_in = dict((normalize_header(k[5:]), v) for k, v in self.environ.items()
                           if k.startswith('HTTP_'))
-        https = environ.get("HTTPS") in ('yes', 'on', '1')
+        https = self.is_secure()
         post, files = self.get_posted_data()
 
         super(CubicWebWsgiRequest, self).__init__(vreg, https, post,
@@ -104,32 +104,8 @@
 
     ## wsgi request helpers ###################################################
 
-    def instance_uri(self):
-        """Return the instance's base URI (no PATH_INFO or QUERY_STRING)
-
-        see python2.5's wsgiref.util.instance_uri code
-        """
-        environ = self.environ
-        url = environ['wsgi.url_scheme'] + '://'
-        if environ.get('HTTP_HOST'):
-            url += environ['HTTP_HOST']
-        else:
-            url += environ['SERVER_NAME']
-            if environ['wsgi.url_scheme'] == 'https':
-                if environ['SERVER_PORT'] != '443':
-                    url += ':' + environ['SERVER_PORT']
-            else:
-                if environ['SERVER_PORT'] != '80':
-                    url += ':' + environ['SERVER_PORT']
-        url += quote(environ.get('SCRIPT_NAME') or '/')
-        return url
-
-    def get_full_path(self):
-        return '%s%s' % (self.path, self.environ.get('QUERY_STRING', '') and ('?' + self.environ.get('QUERY_STRING', '')) or '')
-
     def is_secure(self):
-        return 'wsgi.url_scheme' in self.environ \
-            and self.environ['wsgi.url_scheme'] == 'https'
+        return self.environ['wsgi.url_scheme'] == 'https'
 
     def get_posted_data(self):
         # The WSGI spec says 'QUERY_STRING' may be absent.