# HG changeset patch # User Julien Cristau # Date 1403862506 -7200 # Node ID 2077c8da1893e4bb12c3ef070d9eecf3de556295 # Parent 95902c0b991b636b7865bc4a29c378b8403bce8f# Parent bb719d857421adab24cea63f393938c08dcdfb04 merge from 3.19 branch diff -r bb719d857421 -r 2077c8da1893 migration.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': diff -r bb719d857421 -r 2077c8da1893 req.py --- 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 ############# diff -r bb719d857421 -r 2077c8da1893 server/sources/native.py --- 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)) diff -r bb719d857421 -r 2077c8da1893 server/sqlutils.py --- 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 diff -r bb719d857421 -r 2077c8da1893 test/unittest_dbapi.py --- 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 diff -r bb719d857421 -r 2077c8da1893 test/unittest_toolsutils.py --- /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 . + + +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() diff -r bb719d857421 -r 2077c8da1893 toolsutils.py --- 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\s*(?:rql)' # match rql, possibly indented + r'|' # or + r'\s*(?:\w+\.execute))' # match .execute, possibly indented + # end of + r'\(' # followed by a parenthesis + r'(?P["\'])' # a quote or double quote + r'(?P.*)') # 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 diff -r bb719d857421 -r 2077c8da1893 view.py --- 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__) diff -r bb719d857421 -r 2077c8da1893 web/application.py --- 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 diff -r bb719d857421 -r 2077c8da1893 web/data/cubicweb.ajax.js --- 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.'); diff -r bb719d857421 -r 2077c8da1893 web/data/cubicweb.css --- 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; } diff -r bb719d857421 -r 2077c8da1893 web/data/cubicweb.edition.js --- 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)); }); diff -r bb719d857421 -r 2077c8da1893 web/data/cubicweb.facets.js --- 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); } diff -r bb719d857421 -r 2077c8da1893 web/data/cubicweb.htmlhelpers.js --- 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 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 diff -r bb719d857421 -r 2077c8da1893 web/data/cubicweb.js --- 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]; }, diff -r bb719d857421 -r 2077c8da1893 web/data/cubicweb.old.css --- 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; } diff -r bb719d857421 -r 2077c8da1893 web/data/cubicweb.timeline-bundle.js --- 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 diff -r bb719d857421 -r 2077c8da1893 web/formfields.py --- 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 diff -r bb719d857421 -r 2077c8da1893 web/formwidgets.py --- 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): diff -r bb719d857421 -r 2077c8da1893 web/http_headers.py --- 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 diff -r bb719d857421 -r 2077c8da1893 web/request.py --- 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 diff -r bb719d857421 -r 2077c8da1893 web/test/unittest_http.py --- 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'), ] diff -r bb719d857421 -r 2077c8da1893 web/test/unittest_web.py --- 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() diff -r bb719d857421 -r 2077c8da1893 web/views/ajaxedit.py --- 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'] diff -r bb719d857421 -r 2077c8da1893 web/views/autoform.py --- 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'') w(u'
    ') for viewparams in related: - w(u'' + w(u'' % (viewparams[1], viewparams[0], viewparams[2], viewparams[3])) if not form.force_display and form.maxrelitems < len(related): link = (u'