# HG changeset patch # User Julien Cristau # Date 1411652953 -7200 # Node ID c84ad981fc4a3279c88110a046ef88003cdd4e38 # Parent b240b33c712535e6c1e219711f09c090c90e3ff0# Parent c4e740e50fc7d371d14df17d26bc42d1f8060261 merge 3.19.4 in 3.20 branch diff -r b240b33c7125 -r c84ad981fc4a .hgtags --- a/.hgtags Tue Sep 23 17:34:36 2014 +0200 +++ b/.hgtags Thu Sep 25 15:49:13 2014 +0200 @@ -335,6 +335,9 @@ a979d1594af6501a774fb32eb67cd32fea626655 cubicweb-version-3.17.16 a979d1594af6501a774fb32eb67cd32fea626655 cubicweb-debian-version-3.17.16-1 a979d1594af6501a774fb32eb67cd32fea626655 cubicweb-centos-version-3.17.16-1 +57e9d1c70512d0f4e2c33d33db436a8274e10c1a cubicweb-version-3.17.17 +57e9d1c70512d0f4e2c33d33db436a8274e10c1a cubicweb-debian-version-3.17.17-1 +57e9d1c70512d0f4e2c33d33db436a8274e10c1a cubicweb-centos-version-3.17.17-1 db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-version-3.18.0 db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-debian-version-3.18.0-1 db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-centos-version-3.18.0-1 @@ -353,6 +356,9 @@ 5071b69b6b0b0de937bb231404cbf652a103dbe0 cubicweb-version-3.18.5 5071b69b6b0b0de937bb231404cbf652a103dbe0 cubicweb-debian-version-3.18.5-1 5071b69b6b0b0de937bb231404cbf652a103dbe0 cubicweb-centos-version-3.18.5-1 +d915013567429b481cb2c367071e36451c07a226 cubicweb-version-3.18.6 +d915013567429b481cb2c367071e36451c07a226 cubicweb-debian-version-3.18.6-1 +d915013567429b481cb2c367071e36451c07a226 cubicweb-centos-version-3.18.6-1 1141927b8494aabd16e31b0d0d9a50fe1fed5f2f cubicweb-version-3.19.0 1141927b8494aabd16e31b0d0d9a50fe1fed5f2f cubicweb-debian-version-3.19.0-1 1141927b8494aabd16e31b0d0d9a50fe1fed5f2f cubicweb-centos-version-3.19.0-1 diff -r b240b33c7125 -r c84ad981fc4a __init__.py --- a/__init__.py Tue Sep 23 17:34:36 2014 +0200 +++ b/__init__.py Thu Sep 25 15:49:13 2014 +0200 @@ -123,7 +123,7 @@ def __eq__(self, other): if not isinstance(other, Binary): return False - return self.getvalue(), other.getvalue() + return self.getvalue() == other.getvalue() # Binary helpers to store/fetch python objects diff -r b240b33c7125 -r c84ad981fc4a __pkginfo__.py --- a/__pkginfo__.py Tue Sep 23 17:34:36 2014 +0200 +++ b/__pkginfo__.py Thu Sep 25 15:49:13 2014 +0200 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 19, 3) +numversion = (3, 19, 4) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -39,7 +39,7 @@ ] __depends__ = { - 'logilab-common': '>= 0.60.0', + 'logilab-common': '>= 0.62.0', 'logilab-mtconverter': '>= 0.8.0', 'rql': '>= 0.31.2', 'yams': '>= 0.40.0', diff -r b240b33c7125 -r c84ad981fc4a cubicweb.spec --- a/cubicweb.spec Tue Sep 23 17:34:36 2014 +0200 +++ b/cubicweb.spec Thu Sep 25 15:49:13 2014 +0200 @@ -7,7 +7,7 @@ %endif Name: cubicweb -Version: 3.19.3 +Version: 3.19.4 Release: logilab.1%{?dist} Summary: CubicWeb is a semantic web application framework Source0: http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz @@ -20,7 +20,7 @@ BuildArch: noarch Requires: %{python} -Requires: %{python}-logilab-common >= 0.60.0 +Requires: %{python}-logilab-common >= 0.62.0 Requires: %{python}-logilab-mtconverter >= 0.8.0 Requires: %{python}-rql >= 0.31.2 Requires: %{python}-yams >= 0.40.0 diff -r b240b33c7125 -r c84ad981fc4a cwconfig.py --- a/cwconfig.py Tue Sep 23 17:34:36 2014 +0200 +++ b/cwconfig.py Thu Sep 25 15:49:13 2014 +0200 @@ -124,14 +124,14 @@ Python `````` -If you installed *CubicWeb* by cloning the Mercurial forest or from source +If you installed *CubicWeb* by cloning the Mercurial shell repository or from source distribution, then you will need to update the environment variable PYTHONPATH by -adding the path to the forest `cubicweb`: +adding the path to `cubicweb`: Add the following lines to either :file:`.bashrc` or :file:`.bash_profile` to configure your development environment :: - export PYTHONPATH=/full/path/to/cubicweb-forest + export PYTHONPATH=/full/path/to/grshell-cubicweb If you installed *CubicWeb* with packages, no configuration is required and your new cubes will be placed in `/usr/share/cubicweb/cubes` and your instances will diff -r b240b33c7125 -r c84ad981fc4a cwctl.py --- a/cwctl.py Tue Sep 23 17:34:36 2014 +0200 +++ b/cwctl.py Thu Sep 25 15:49:13 2014 +0200 @@ -396,13 +396,14 @@ print helper.bootstrap(cubes, self.config.automatic, self.config.config_level) # input for cubes specific options - sections = set(sect.lower() for sect, opt, odict in config.all_options() - if 'type' in odict - and odict.get('level') <= self.config.config_level) - for section in sections: - if section not in ('main', 'email', 'pyro', 'web'): - print '\n' + underline_title('%s options' % section) - config.input_config(section, self.config.config_level) + if not self.config.automatic: + sections = set(sect.lower() for sect, opt, odict in config.all_options() + if 'type' in odict + and odict.get('level') <= self.config.config_level) + for section in sections: + if section not in ('main', 'email', 'pyro', 'web'): + print '\n' + underline_title('%s options' % section) + config.input_config(section, self.config.config_level) # write down configuration config.save() self._handle_win32(config, appid) @@ -1050,12 +1051,16 @@ return ('stdlib',) return ('stdlib', 'werkzeug') -class WSGIDebugStartHandler(InstanceCommand): +class WSGIStartHandler(InstanceCommand): """Start an interactive wsgi server """ name = 'wsgi' actionverb = 'started' arguments = '' options = ( + ("debug", + {'short': 'D', 'action': 'store_true', + 'default': False, + 'help': 'start server in debug mode.'}), ('method', {'short': 'm', 'type': 'choice', @@ -1065,16 +1070,16 @@ 'help': 'wsgi utility/method'}), ('loglevel', {'short': 'l', - 'type' : 'choice', + 'type': 'choice', 'metavar': '', - 'default': 'debug', + 'default': None, 'choices': ('debug', 'info', 'warning', 'error'), 'help': 'debug if -D is set, error otherwise', }), ) def wsgi_instance(self, appid): - config = cwcfg.config_for(appid, debugmode=1) + config = cwcfg.config_for(appid, debugmode=self['debug']) init_cmdline_log_threshold(config, self['loglevel']) assert config.name == 'all-in-one' meth = self['method'] @@ -1089,7 +1094,7 @@ for cmdcls in (ListCommand, CreateInstanceCommand, DeleteInstanceCommand, StartInstanceCommand, StopInstanceCommand, RestartInstanceCommand, - WSGIDebugStartHandler, + WSGIStartHandler, ReloadConfigurationCommand, StatusCommand, UpgradeInstanceCommand, ListVersionsInstanceCommand, diff -r b240b33c7125 -r c84ad981fc4a cwvreg.py --- a/cwvreg.py Tue Sep 23 17:34:36 2014 +0200 +++ b/cwvreg.py Thu Sep 25 15:49:13 2014 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -241,7 +241,7 @@ class CWRegistry(Registry): def __init__(self, vreg): - super(CWRegistry, self).__init__(vreg.config.debugmode) + super(CWRegistry, self).__init__(True) self.vreg = vreg @property @@ -598,11 +598,33 @@ if self.is_reload_needed(path): self.reload(path) + def _cleanup_sys_modules(self, path): + """Remove submodules of `directories` from `sys.modules` and cleanup + CW_EVENT_MANAGER accordingly. + + We take care to properly remove obsolete registry callbacks. + + """ + caches = {} + callbackdata = CW_EVENT_MANAGER.callbacks.values() + for callbacklist in callbackdata: + for callback in callbacklist: + func = callback[0] + # for non-function callable, we do nothing interesting + module = getattr(func, '__module__', None) + caches[id(callback)] = module + deleted_modules = set(cleanup_sys_modules(path)) + for callbacklist in callbackdata: + for callback in callbacklist[:]: + module = caches[id(callback)] + if module and module in deleted_modules: + callbacklist.remove(callback) + def reload(self, path, force_reload=True): """modification detected, reset and reload the vreg""" CW_EVENT_MANAGER.emit('before-registry-reload') if force_reload: - cleanup_sys_modules(path) + self._cleanup_sys_modules(path) cubes = self.config.cubes() # if the fs code use some cubes not yet registered into the instance # we should cleanup sys.modules for those as well to avoid potential @@ -611,7 +633,7 @@ for cube in cfg.expand_cubes(cubes, with_recommends=True): if not cube in cubes: cpath = cfg.build_appobjects_cube_path([cfg.cube_dir(cube)]) - cleanup_sys_modules(cpath) + self._cleanup_sys_modules(cpath) self.register_objects(path) CW_EVENT_MANAGER.emit('after-registry-reload') diff -r b240b33c7125 -r c84ad981fc4a debian/changelog --- a/debian/changelog Tue Sep 23 17:34:36 2014 +0200 +++ b/debian/changelog Thu Sep 25 15:49:13 2014 +0200 @@ -1,3 +1,9 @@ +cubicweb (3.19.4-1) unstable; urgency=low + + * new upstream release + + -- Julien Cristau Thu, 25 Sep 2014 14:24:04 +0200 + cubicweb (3.19.3-1) unstable; urgency=low * new upstream release @@ -22,6 +28,12 @@ -- Julien Cristau Mon, 28 Apr 2014 18:35:27 +0200 +cubicweb (3.18.6-1) unstable; urgency=low + + * new upstream release + + -- Julien Cristau Wed, 24 Sep 2014 15:08:34 +0200 + cubicweb (3.18.5-1) unstable; urgency=low * new upstream release @@ -58,9 +70,15 @@ -- Julien Cristau Fri, 10 Jan 2014 17:14:18 +0100 +cubicweb (3.17.17-1) unstable; urgency=low + + * new upstream release + + -- Aurelien Campeas Tue, 16 Sep 2014 18:38:19 +0200 + cubicweb (3.17.16-1) unstable; urgency=low - * new upstream value + * new upstream release -- Aurelien Campeas Mon, 07 Jul 2014 19:26:12 +0200 diff -r b240b33c7125 -r c84ad981fc4a debian/control --- a/debian/control Tue Sep 23 17:34:36 2014 +0200 +++ b/debian/control Thu Sep 25 15:49:13 2014 +0200 @@ -151,7 +151,7 @@ graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), - python-logilab-common (>= 0.60.0), + python-logilab-common (>= 0.62.0), python-yams (>= 0.40.0), python-rql (>= 0.31.2), python-lxml diff -r b240b33c7125 -r c84ad981fc4a devtools/httptest.py --- a/devtools/httptest.py Tue Sep 23 17:34:36 2014 +0200 +++ b/devtools/httptest.py Thu Sep 25 15:49:13 2014 +0200 @@ -89,8 +89,6 @@ * `anonymous_allowed`: flag telling if anonymous browsing should be allowed """ configcls = CubicWebServerConfig - # anonymous is logged by default in cubicweb test cases - anonymous_allowed = True def start_server(self): # use a semaphore to avoid starting test while the http server isn't @@ -185,8 +183,3 @@ # Server could be launched manually print err super(CubicWebServerTC, self).tearDown() - - @classmethod - def init_config(cls, config): - config.set_anonymous_allowed(cls.anonymous_allowed) - super(CubicWebServerTC, cls).init_config(config) diff -r b240b33c7125 -r c84ad981fc4a devtools/test/unittest_webtest.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/test/unittest_webtest.py Thu Sep 25 15:49:13 2014 +0200 @@ -0,0 +1,40 @@ +import httplib + +from logilab.common.testlib import Tags +from cubicweb.devtools.webtest import CubicWebTestTC + + +class CWTTC(CubicWebTestTC): + def test_response(self): + response = self.webapp.get('/') + self.assertEqual(200, response.status_int) + + def test_base_url(self): + if self.config['base-url'] not in self.webapp.get('/').text: + self.fail('no mention of base url in retrieved page') + + +class CWTIdentTC(CubicWebTestTC): + anonymous_allowed = False + tags = CubicWebTestTC.tags | Tags(('auth',)) + + def test_reponse_denied(self): + res = self.webapp.get('/', expect_errors=True) + self.assertEqual(httplib.FORBIDDEN, res.status_int) + + def test_login(self): + res = self.webapp.get('/', expect_errors=True) + self.assertEqual(httplib.FORBIDDEN, res.status_int) + + self.login(self.admlogin, self.admpassword) + res = self.webapp.get('/') + self.assertEqual(httplib.OK, res.status_int) + + self.logout() + res = self.webapp.get('/', expect_errors=True) + self.assertEqual(httplib.FORBIDDEN, res.status_int) + + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff -r b240b33c7125 -r c84ad981fc4a devtools/testlib.py --- a/devtools/testlib.py Tue Sep 23 17:34:36 2014 +0200 +++ b/devtools/testlib.py Thu Sep 25 15:49:13 2014 +0200 @@ -297,6 +297,9 @@ _cnxs = set() # establised connection # stay on connection for leak detection purpose + # anonymous is logged by default in cubicweb test cases + anonymous_allowed = True + def __init__(self, *args, **kwargs): self._admin_session = None self._admin_clt_cnx = None @@ -532,6 +535,7 @@ config.global_set_option('embed-allowed', re.compile('.*')) except Exception: # not in server only configuration pass + config.set_anonymous_allowed(cls.anonymous_allowed) @property def vreg(self): @@ -610,7 +614,7 @@ """add your database setup code by overriding this method""" @classmethod - def pre_setup_database(cls, session, config): + def pre_setup_database(cls, cnx, config): """add your pre database setup code by overriding this method Do not forget to set the cls.test_db_id value to enable caching of the @@ -879,6 +883,7 @@ raise return result + @deprecated('[3.19] use .admin_request_from_url instead') def req_from_url(self, url): """parses `url` and builds the corresponding CW-web request @@ -892,6 +897,20 @@ req.setup_params(params) return req + @contextmanager + def admin_request_from_url(self, url): + """parses `url` and builds the corresponding CW-web request + + req.form will be setup using the url's query string + """ + with self.admin_access.web_request(url=url) as req: + if isinstance(url, unicode): + url = url.encode(req.encoding) # req.setup_params() expects encoded strings + querystring = urlparse.urlparse(url)[-2] + params = urlparse.parse_qs(querystring) + req.setup_params(params) + yield req + def url_publish(self, url, data=None): """takes `url`, uses application's app_resolver to find the appropriate controller and result set, then publishes the result. @@ -902,22 +921,22 @@ This should pretty much correspond to what occurs in a real CW server except the apache-rewriter component is not called. """ - req = self.req_from_url(url) - if data is not None: - req.form.update(data) - ctrlid, rset = self.app.url_resolver.process(req, req.relative_path(False)) - return self.ctrl_publish(req, ctrlid, rset) + with self.admin_request_from_url(url) as req: + if data is not None: + req.form.update(data) + ctrlid, rset = self.app.url_resolver.process(req, req.relative_path(False)) + return self.ctrl_publish(req, ctrlid, rset) def http_publish(self, url, data=None): """like `url_publish`, except this returns a http response, even in case of errors. You may give form parameters using the `data` argument. """ - req = self.req_from_url(url) - if data is not None: - req.form.update(data) - with real_error_handling(self.app): - result = self.app_handle_request(req, req.relative_path(False)) - return result, req + with self.admin_request_from_url(url) as req: + if data is not None: + req.form.update(data) + with real_error_handling(self.app): + result = self.app_handle_request(req, req.relative_path(False)) + return result, req @staticmethod def _parse_location(req, location): diff -r b240b33c7125 -r c84ad981fc4a devtools/webtest.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/webtest.py Thu Sep 25 15:49:13 2014 +0200 @@ -0,0 +1,36 @@ +from __future__ import absolute_import + +import webtest + +from cubicweb.wsgi import handler +from cubicweb.devtools.testlib import CubicWebTC + + +class CubicWebTestTC(CubicWebTC): + @classmethod + def init_config(cls, config): + super(CubicWebTestTC, cls).init_config(config) + config.global_set_option('base-url', 'http://localhost.local/') + + def setUp(self): + super(CubicWebTestTC, self).setUp() + webapp = handler.CubicWebWSGIApplication(self.config) + self.webapp = webtest.TestApp(webapp) + + def tearDown(self): + del self.webapp + super(CubicWebTestTC, self).tearDown() + + def login(self, user=None, password=None, **args): + if user is None: + user = self.admlogin + if password is None: + password = self.admpassword if user == self.admlogin else user + args.update({ + '__login': user, + '__password': password + }) + return self.webapp.get('/login', args) + + def logout(self): + return self.webapp.get('/logout') diff -r b240b33c7125 -r c84ad981fc4a doc/book/en/admin/cubicweb-ctl.rst --- a/doc/book/en/admin/cubicweb-ctl.rst Tue Sep 23 17:34:36 2014 +0200 +++ b/doc/book/en/admin/cubicweb-ctl.rst Thu Sep 25 15:49:13 2014 +0200 @@ -37,7 +37,7 @@ cubicweb-ctl newcube This will create a new cube in -``/path/to/forest/cubicweb/cubes/`` for a Mercurial forest +``/path/to/grshell-cubicweb/cubes/`` for a Mercurial installation, or in ``/usr/share/cubicweb/cubes`` for a debian packages installation. diff -r b240b33c7125 -r c84ad981fc4a doc/book/en/admin/setup-windows.rst --- a/doc/book/en/admin/setup-windows.rst Tue Sep 23 17:34:36 2014 +0200 +++ b/doc/book/en/admin/setup-windows.rst Thu Sep 25 15:49:13 2014 +0200 @@ -23,12 +23,12 @@ |cubicweb| requires some base elements that must be installed to run correctly. So, first of all, you must install them : -* python >= 2.5 and < 3 +* python >= 2.6 and < 3 (`Download Python `_). You can also consider the Python(x,y) distribution (`Download Python(x,y) `_) as it makes things easier for Windows user by wrapping in a single installer - python 2.5 plus numerous useful third-party modules and + python 2.7 plus numerous useful third-party modules and applications (including Eclipse + pydev, which is an arguably good IDE for Python under Windows). @@ -40,7 +40,7 @@ (version >=2.2.1) allows working with XML and HTML (`Download lxml `_) -* `Postgresql 8.4 `_, +* `Postgresql `_, an object-relational database system (`Download Postgresql `_) and its python drivers @@ -50,8 +50,7 @@ (`Download gettext `_). * `rql `_, - the recent version of the Relationship Query Language parser - (`Download rql `_). + the recent version of the Relationship Query Language parser. Install optional elements ------------------------- @@ -59,16 +58,6 @@ We recommend you to install the following elements. They are not mandatory but they activate very interesting features in |cubicweb|: -* `Simplejson `_ - must be installed if you have python <= 2.5 - (`Download simplejson `_). - It is included in the Standard library from Python >= 2.6. - -* `Pyro `_ - enables remote access to cubicweb repository instances. - It also allows the client and the server not running on the same machine - (`Download Pyro `_). - * `python-ldap `_ provides access to LDAP/Active directory directories (`Download python-ldap `_). diff -r b240b33c7125 -r c84ad981fc4a doc/book/en/annexes/depends.rst --- a/doc/book/en/annexes/depends.rst Tue Sep 23 17:34:36 2014 +0200 +++ b/doc/book/en/annexes/depends.rst Thu Sep 25 15:49:13 2014 +0200 @@ -6,7 +6,7 @@ ========================= When you run CubicWeb from source, either by downloading the tarball or -cloning the mercurial forest, here is the list of tools and libraries you need +cloning the mercurial tree, here is the list of tools and libraries you need to have installed in order for CubicWeb to work: * yapps - http://theory.stanford.edu/~amitp/yapps/ - @@ -15,61 +15,43 @@ * pygraphviz - http://networkx.lanl.gov/pygraphviz/ - http://pypi.python.org/pypi/pygraphviz -* simplejson - http://code.google.com/p/simplejson/ - - http://pypi.python.org/pypi/simplejson - -* docsutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils +* docutils - http://docutils.sourceforge.net/ - http://pypi.python.org/pypi/docutils * lxml - http://codespeak.net/lxml - http://pypi.python.org/pypi/lxml * twisted - http://twistedmatrix.com/ - http://pypi.python.org/pypi/Twisted * logilab-common - http://www.logilab.org/project/logilab-common - - http://pypi.python.org/pypi/logilab-common/ - included in the forest + http://pypi.python.org/pypi/logilab-common/ * logilab-database - http://www.logilab.org/project/logilab-database - - http://pypi.python.org/pypi/logilab-database/ - included in the forest + http://pypi.python.org/pypi/logilab-database/ * logilab-constraint - http://www.logilab.org/project/logilab-constraint - - http://pypi.python.org/pypi/constraint/ - included in the forest + http://pypi.python.org/pypi/constraint/ * logilab-mtconverter - http://www.logilab.org/project/logilab-mtconverter - - http://pypi.python.org/pypi/logilab-mtconverter - included in the forest + http://pypi.python.org/pypi/logilab-mtconverter -* rql - http://www.logilab.org/project/rql - http://pypi.python.org/pypi/rql - - included in the forest +* rql - http://www.logilab.org/project/rql - http://pypi.python.org/pypi/rql * yams - http://www.logilab.org/project/yams - http://pypi.python.org/pypi/yams - - included in the forest * indexer - http://www.logilab.org/project/indexer - - http://pypi.python.org/pypi/indexer - included in the forest + http://pypi.python.org/pypi/indexer * passlib - https://code.google.com/p/passlib/ - http://pypi.python.org/pypi/passlib -To use network communication between cubicweb instances / clients: - -* Pyro - http://www.xs4all.nl/~irmen/pyro3/ - http://pypi.python.org/pypi/Pyro - -If you're using a Postgres database (recommended): +If you're using a Postgresql database (recommended): * psycopg2 - http://initd.org/projects/psycopg2 - http://pypi.python.org/pypi/psycopg2 * plpythonu extension -* tsearch2 extension (for postgres < 8.3, in postgres-contrib) Other optional packages: * fyzz - http://www.logilab.org/project/fyzz - - http://pypi.python.org/pypi/fyzz - included in the forest, *to activate Sparql querying* - -For the google-appengine extension to be available, you also need: - -* vobject - http://vobject.skyhouseconsulting.com/ - - http://pypi.python.org/pypi/vobject, *for the icalendar view*. For those not - benefiting from a packaging system, note that vobject itself depends on - dateutil - http://labix.org/python-dateutil - - http://pypi.python.org/pypi/python-dateutil/. + http://pypi.python.org/pypi/fyzz *to activate Sparql querying* Any help with the packaging of CubicWeb for more than Debian/Ubuntu (including diff -r b240b33c7125 -r c84ad981fc4a doc/book/en/annexes/mercurial.rst --- a/doc/book/en/annexes/mercurial.rst Tue Sep 23 17:34:36 2014 +0200 +++ b/doc/book/en/annexes/mercurial.rst Thu Sep 25 15:49:13 2014 +0200 @@ -18,13 +18,13 @@ .. _Mercurial: http://www.selenic.com/mercurial/ -In contrast to CVS/Subversion, we usually create a repository by +In contrast to CVS/Subversion, we usually create a repository per project to manage. In a collaborative development, we usually create a central repository accessible to all developers of the project. These central repository is used -as a reference. According to its needs, then everyone can have a local repository, -that you will have to synchronize with the central repository from time to time. +as a reference. According to their needs, everyone can have a local repository, +that they will have to synchronize with the central repository from time to time. Major commands @@ -33,7 +33,7 @@ hg clone ssh://myhost//home/src/repo -* See the contents of the local repository (graphical tool in Tk):: +* See the contents of the local repository (graphical tool in Qt):: hgview @@ -111,17 +111,15 @@ 3. `hg ci` 4. `hg push` -Installation of the forest extension -```````````````````````````````````` +Installation of the guestrepo extension +``````````````````````````````````````` -Set up the forest extension by getting a copy of the sources -from http://hg.akoha.org/hgforest/ and adding the following +Set up the guestrepo extension by getting a copy of the sources +from https://bitbucket.org/selinc/guestrepo and adding the following lines to your ``~/.hgrc``: :: [extensions] - hgext.forest= - # or, if forest.py is not in the hgext dir: - # forest=/path/to/forest.py + guestrepo=/path/to/guestrepo/guestrepo More information diff -r b240b33c7125 -r c84ad981fc4a doc/book/en/devrepo/datamodel/definition.rst --- a/doc/book/en/devrepo/datamodel/definition.rst Tue Sep 23 17:34:36 2014 +0200 +++ b/doc/book/en/devrepo/datamodel/definition.rst Thu Sep 25 15:49:13 2014 +0200 @@ -300,7 +300,7 @@ * users and groups of users * a user belongs to at least one group of user -* permissions (read, update, create, delete) +* permissions (`read`, `update`, `create`, `delete`) * permissions are assigned to groups (and not to users) For *CubicWeb* in particular: @@ -320,10 +320,10 @@ * the permissions of this group are only checked on `update`/`delete` actions if all the other groups the user belongs to do not provide those permissions -Setting permissions is done with the attribute `__permissions__` of entities and -relation definition. The value of this attribute is a dictionary where the keys -are the access types (action), and the values are the authorized groups or -expressions. +Setting permissions is done with the class attribute `__permissions__` +of entity types and relation definitions. The value of this attribute +is a dictionary where the keys are the access types (action), and the +values are the authorized groups or rql expressions. For an entity type, the possible actions are `read`, `add`, `update` and `delete`. @@ -333,6 +333,19 @@ For an attribute, the possible actions are `read`, `add` and `update`, and they are a refinement of an entity type permission. +.. note:: + + By default, the permissions of an entity type attributes are + equivalent to the permissions of the entity type itself. + + It is possible to provide custom attribute permissions which are + stronger than, or are more lenient than the entity type + permissions. + + In a situation where all attributes were given custom permissions, + the entity type permissions would not be checked, except for the + `delete` action. + For each access type, a tuple indicates the name of the authorized groups and/or one or multiple RQL expressions to satisfy to grant access. The access is provided if the user is in one of the listed groups or if one of the RQL condition @@ -368,6 +381,13 @@ 'add': ('managers', ERQLExpression('U has_add_permission X'), 'update': ('managers', ERQLExpression('U has_update_permission X')),} +.. note:: + + The default permissions for attributes are not syntactically + equivalent to the default permissions of the entity types, but the + rql expressions work by delegating to the entity type permissions. + + The standard user groups ```````````````````````` diff -r b240b33c7125 -r c84ad981fc4a doc/book/en/tutorials/base/customizing-the-application.rst --- a/doc/book/en/tutorials/base/customizing-the-application.rst Tue Sep 23 17:34:36 2014 +0200 +++ b/doc/book/en/tutorials/base/customizing-the-application.rst Thu Sep 25 15:49:13 2014 +0200 @@ -26,7 +26,7 @@ cubicweb-ctl newcube myblog -This will create in the cubes directory (:file:`/path/to/forest/cubes` for source +This will create in the cubes directory (:file:`/path/to/grshell/cubes` for source installation, :file:`/usr/share/cubicweb/cubes` for Debian packages installation) a directory named :file:`blog` reflecting the structure described in :ref:`cubelayout`. diff -r b240b33c7125 -r c84ad981fc4a entities/adapters.py --- a/entities/adapters.py Tue Sep 23 17:34:36 2014 +0200 +++ b/entities/adapters.py Thu Sep 25 15:49:13 2014 +0200 @@ -367,15 +367,3 @@ globalmsg = _('some relations violate a unicity constraint') rtypes_msg['unicity constraint'] = globalmsg raise ValidationError(self.entity.eid, rtypes_msg) - -# deprecated ################################################################### - - -class adapter_deprecated(view.auto_unwrap_bw_compat): - """metaclass to print a warning on instantiation of a deprecated class""" - - def __call__(cls, *args, **kwargs): - msg = getattr(cls, "__deprecation_warning__", - "%(cls)s is deprecated") % {'cls': cls.__name__} - warn(msg, DeprecationWarning, stacklevel=2) - return type.__call__(cls, *args, **kwargs) diff -r b240b33c7125 -r c84ad981fc4a entity.py --- a/entity.py Tue Sep 23 17:34:36 2014 +0200 +++ b/entity.py Thu Sep 25 15:49:13 2014 +0200 @@ -425,8 +425,10 @@ needcheck = not cls.e_schema.has_unique_values(mainattr) else: for rschema in cls.e_schema.subject_relations(): - if rschema.final and rschema != 'eid' \ - and cls.e_schema.has_unique_values(rschema): + if (rschema.final + and rschema != 'eid' + and cls.e_schema.has_unique_values(rschema) + and cls.e_schema.rdef(rschema.type).cardinality[0] == '1'): mainattr = str(rschema) needcheck = False break diff -r b240b33c7125 -r c84ad981fc4a etwist/server.py --- a/etwist/server.py Tue Sep 23 17:34:36 2014 +0200 +++ b/etwist/server.py Thu Sep 25 15:49:13 2014 +0200 @@ -178,7 +178,7 @@ path = self.channel._path.split('?', 1)[0].rstrip('/').rsplit('/', 1)[-1] self.clientproto = 'HTTP/1.1' # not yet initialized self.channel.persistent = 0 # force connection close on cleanup - self.setResponseCode(http.BAD_REQUEST) + self.setResponseCode(http.REQUEST_ENTITY_TOO_LARGE) if path in JSON_PATHS: # XXX better json path detection self.setHeader('content-type',"application/json") body = json_dumps({'reason': 'request max size exceeded'}) diff -r b240b33c7125 -r c84ad981fc4a hooks/email.py diff -r b240b33c7125 -r c84ad981fc4a hooks/security.py --- a/hooks/security.py Tue Sep 23 17:34:36 2014 +0200 +++ b/hooks/security.py Thu Sep 25 15:49:13 2014 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -34,11 +34,15 @@ def check_entity_attributes(cnx, entity, action, editedattrs=None): eid = entity.eid eschema = entity.e_schema + if action == 'delete': + eschema.check_perm(session, action, eid=eid) + return # ._cw_skip_security_attributes is there to bypass security for attributes # set by hooks by modifying the entity's dictionary if editedattrs is None: editedattrs = entity.cw_edited dontcheck = editedattrs.skip_security + etypechecked = False for attr in editedattrs: if attr in dontcheck: continue @@ -54,10 +58,10 @@ # implements comparison by rql expression. if perms == buildobjs.DEFAULT_ATTRPERMS[action]: # The default rule is to delegate to the entity - # rule. This is an historical artefact. Hence we take - # this object as a marker saying "no specific" - # permission rule for this attribute. Thus we just do - # nothing. + # rule. This needs to be checked only once. + if not etypechecked: + entity.cw_check_perm(action) + etypechecked = True continue if perms == (): # That means an immutable attribute; as an optimization, avoid @@ -71,7 +75,6 @@ cnx = self.cnx for eid, action, edited in self.get_data(): entity = cnx.entity_from_eid(eid) - entity.cw_check_perm(action) check_entity_attributes(cnx, entity, action, edited) diff -r b240b33c7125 -r c84ad981fc4a hooks/syncsession.py --- a/hooks/syncsession.py Tue Sep 23 17:34:36 2014 +0200 +++ b/hooks/syncsession.py Thu Sep 25 15:49:13 2014 +0200 @@ -42,15 +42,15 @@ """base class for group operation""" cnxuser = None # make pylint happy - def __init__(self, session, *args, **kwargs): + def __init__(self, cnx, *args, **kwargs): """override to get the group name before actual groups manipulation: we may temporarily loose right access during a commit event, so no query should be emitted while comitting """ rql = 'Any N WHERE G eid %(x)s, G name N' - result = session.execute(rql, {'x': kwargs['geid']}, build_descr=False) - hook.Operation.__init__(self, session, *args, **kwargs) + result = cnx.execute(rql, {'x': kwargs['geid']}, build_descr=False) + hook.Operation.__init__(self, cnx, *args, **kwargs) self.group = result[0][0] @@ -94,14 +94,14 @@ class _DelUserOp(hook.Operation): """close associated user's session when it is deleted""" - def __init__(self, session, cnxid): - self.cnxid = cnxid - hook.Operation.__init__(self, session) + def __init__(self, cnx, sessionid): + self.sessionid = sessionid + hook.Operation.__init__(self, cnx) def postcommit_event(self): """the observed connections set has been commited""" try: - self.session.repo.close(self.cnxid) + self.cnx.repo.close(self.sessionid) except BadConnectionId: pass # already closed @@ -148,7 +148,7 @@ """the observed connections set has been commited""" cwprop = self.cwprop if not cwprop.for_user: - self.session.vreg['propertyvalues'][cwprop.pkey] = cwprop.value + self.cnx.vreg['propertyvalues'][cwprop.pkey] = cwprop.value # if for_user is set, update is handled by a ChangeCWPropertyOp operation @@ -161,19 +161,19 @@ key, value = self.entity.pkey, self.entity.value if key.startswith('sources.'): return - session = self._cw + cnx = self._cw try: - value = session.vreg.typed_value(key, value) + value = cnx.vreg.typed_value(key, value) except UnknownProperty: msg = _('unknown property key %s') raise validation_error(self.entity, {('pkey', 'subject'): msg}, (key,)) except ValueError as ex: raise validation_error(self.entity, {('value', 'subject'): str(ex)}) - if not session.user.matching_groups('managers'): - session.add_relation(self.entity.eid, 'for_user', session.user.eid) + if not cnx.user.matching_groups('managers'): + cnx.add_relation(self.entity.eid, 'for_user', cnx.user.eid) else: - _AddCWPropertyOp(session, cwprop=self.entity) + _AddCWPropertyOp(cnx, cwprop=self.entity) class UpdateCWPropertyHook(AddCWPropertyHook): @@ -188,20 +188,20 @@ key, value = entity.pkey, entity.value if key.startswith('sources.'): return - session = self._cw + cnx = self._cw try: - value = session.vreg.typed_value(key, value) + value = cnx.vreg.typed_value(key, value) except UnknownProperty: return except ValueError as ex: raise validation_error(entity, {('value', 'subject'): str(ex)}) if entity.for_user: - for session_ in get_user_sessions(session.repo, entity.for_user[0].eid): - _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties, + for session in get_user_sessions(cnx.repo, entity.for_user[0].eid): + _ChangeCWPropertyOp(cnx, cwpropdict=session.user.properties, key=key, value=value) else: # site wide properties - _ChangeCWPropertyOp(session, cwpropdict=session.vreg['propertyvalues'], + _ChangeCWPropertyOp(cnx, cwpropdict=cnx.vreg['propertyvalues'], key=key, value=value) @@ -211,13 +211,13 @@ def __call__(self): eid = self.entity.eid - session = self._cw - for eidfrom, rtype, eidto in session.transaction_data.get('pendingrelations', ()): + cnx = self._cw + for eidfrom, rtype, eidto in cnx.transaction_data.get('pendingrelations', ()): if rtype == 'for_user' and eidfrom == self.entity.eid: # if for_user was set, delete has already been handled break else: - _DelCWPropertyOp(session, cwpropdict=session.vreg['propertyvalues'], + _DelCWPropertyOp(cnx, cwpropdict=cnx.vreg['propertyvalues'], key=self.entity.pkey) @@ -227,17 +227,17 @@ events = ('after_add_relation',) def __call__(self): - session = self._cw + cnx = self._cw eidfrom = self.eidfrom - if not session.entity_metas(eidfrom)['type'] == 'CWProperty': + if not cnx.entity_metas(eidfrom)['type'] == 'CWProperty': return - key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V', + key, value = cnx.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V', {'x': eidfrom})[0] - if session.vreg.property_info(key)['sitewide']: + if cnx.vreg.property_info(key)['sitewide']: msg = _("site-wide property can't be set for user") raise validation_error(eidfrom, {('for_user', 'subject'): msg}) - for session_ in get_user_sessions(session.repo, self.eidto): - _ChangeCWPropertyOp(session, cwpropdict=session_.user.properties, + for session in get_user_sessions(cnx.repo, self.eidto): + _ChangeCWPropertyOp(cnx, cwpropdict=session.user.properties, key=key, value=value) @@ -246,10 +246,10 @@ events = ('after_delete_relation',) def __call__(self): - session = self._cw - key = session.execute('Any K WHERE P eid %(x)s, P pkey K', + cnx = self._cw + key = cnx.execute('Any K WHERE P eid %(x)s, P pkey K', {'x': self.eidfrom})[0][0] - session.transaction_data.setdefault('pendingrelations', []).append( + cnx.transaction_data.setdefault('pendingrelations', []).append( (self.eidfrom, self.rtype, self.eidto)) - for session_ in get_user_sessions(session.repo, self.eidto): - _DelCWPropertyOp(session, cwpropdict=session_.user.properties, key=key) + for session in get_user_sessions(cnx.repo, self.eidto): + _DelCWPropertyOp(cnx, cwpropdict=session.user.properties, key=key) diff -r b240b33c7125 -r c84ad981fc4a hooks/test/unittest_hooks.py --- a/hooks/test/unittest_hooks.py Tue Sep 23 17:34:36 2014 +0200 +++ b/hooks/test/unittest_hooks.py Thu Sep 25 15:49:13 2014 +0200 @@ -122,14 +122,14 @@ entity = cnx.create_entity('Bookmark', title=u'wf1', path=u'/view') cnx.commit() # fire operations self.assertEqual(len(entity.created_by), 1) # make sure we have only one creator - self.assertEqual(entity.created_by[0].eid, self.session.user.eid) + self.assertEqual(entity.created_by[0].eid, cnx.user.eid) def test_metadata_owned_by(self): with self.admin_access.repo_cnx() as cnx: entity = cnx.create_entity('Bookmark', title=u'wf1', path=u'/view') cnx.commit() # fire operations self.assertEqual(len(entity.owned_by), 1) # make sure we have only one owner - self.assertEqual(entity.owned_by[0].eid, self.session.user.eid) + self.assertEqual(entity.owned_by[0].eid, cnx.user.eid) def test_user_login_stripped(self): with self.admin_access.repo_cnx() as cnx: @@ -153,7 +153,7 @@ self.repo.connect, u'toto', password='hop') cnx.commit() cnxid = self.repo.connect(u'toto', password='hop') - self.assertNotEqual(cnxid, self.session.id) + self.assertNotEqual(cnxid, cnx.sessionid) cnx.execute('DELETE CWUser X WHERE X login "toto"') self.repo.execute(cnxid, 'State X') cnx.commit() diff -r b240b33c7125 -r c84ad981fc4a multipart.py --- a/multipart.py Tue Sep 23 17:34:36 2014 +0200 +++ b/multipart.py Thu Sep 25 15:49:13 2014 +0200 @@ -398,13 +398,13 @@ mem_limit = kw.get('mem_limit', 2**20) if content_length > mem_limit: raise MultipartError("Request to big. Increase MAXMEM.") - data = stream.read(mem_limit).decode(charset) + data = stream.read(mem_limit) if stream.read(1): # These is more that does not fit mem_limit raise MultipartError("Request to big. Increase MAXMEM.") data = parse_qs(data, keep_blank_values=True) for key, values in data.iteritems(): for value in values: - forms[key] = value + forms[key] = value.decode(charset) else: raise MultipartError("Unsupported content type.") except MultipartError: diff -r b240b33c7125 -r c84ad981fc4a predicates.py diff -r b240b33c7125 -r c84ad981fc4a schema.py diff -r b240b33c7125 -r c84ad981fc4a server/querier.py diff -r b240b33c7125 -r c84ad981fc4a server/repository.py diff -r b240b33c7125 -r c84ad981fc4a server/serverctl.py --- a/server/serverctl.py Tue Sep 23 17:34:36 2014 +0200 +++ b/server/serverctl.py Thu Sep 25 15:49:13 2014 +0200 @@ -423,7 +423,7 @@ 'question.', }), ('config-level', - {'short': 'l', 'type': 'int', 'default': 1, + {'short': 'l', 'type': 'int', 'default': 0, 'help': 'level threshold for questions asked when configuring ' 'another source' }), diff -r b240b33c7125 -r c84ad981fc4a server/session.py --- a/server/session.py Tue Sep 23 17:34:36 2014 +0200 +++ b/server/session.py Thu Sep 25 15:49:13 2014 +0200 @@ -532,8 +532,7 @@ def __exit__(self, exctype=None, excvalue=None, tb=None): assert self._open # actually already open assert self._cnxset_count == 0 - self._free_cnxset(ignoremode=True) - self.clear() + self.rollback() self._open = False @@ -1712,10 +1711,13 @@ @property def anonymous_session(self): - # XXX for now, anonymous-user is a web side option. + # XXX for now, anonymous_user only exists in webconfig (and testconfig). # It will only be present inside all-in-one instance. # there is plan to move it down to global config. - return self.user.login == self.repo.config.get('anonymous-user') + if not hasattr(self.repo.config, 'anonymous_user'): + # not a web or test config, no anonymous user + return False + return self.user.login == self.repo.config.anonymous_user()[0] @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)') def schema_rproperty(self, rtype, eidfrom, eidto, rprop): diff -r b240b33c7125 -r c84ad981fc4a server/sources/datafeed.py --- a/server/sources/datafeed.py Tue Sep 23 17:34:36 2014 +0200 +++ b/server/sources/datafeed.py Thu Sep 25 15:49:13 2014 +0200 @@ -421,7 +421,7 @@ self.warning('delete %s %s entities', len(eids), etype) cnx.execute('DELETE %s X WHERE X eid IN (%s)' % (etype, ','.join(eids))) - cnx.commit() + cnx.commit() def update_if_necessary(self, entity, attrs): entity.complete(tuple(attrs)) diff -r b240b33c7125 -r c84ad981fc4a server/sources/native.py --- a/server/sources/native.py Tue Sep 23 17:34:36 2014 +0200 +++ b/server/sources/native.py Thu Sep 25 15:49:13 2014 +0200 @@ -25,12 +25,8 @@ """ __docformat__ = "restructuredtext en" -try: - from cPickle import loads, dumps - import cPickle as pickle -except ImportError: - from pickle import loads, dumps - import pickle +from cPickle import loads, dumps +import cPickle as pickle from threading import Lock from datetime import datetime from base64 import b64decode, b64encode diff -r b240b33c7125 -r c84ad981fc4a server/test/data/migratedapp/schema.py --- a/server/test/data/migratedapp/schema.py Tue Sep 23 17:34:36 2014 +0200 +++ b/server/test/data/migratedapp/schema.py Thu Sep 25 15:49:13 2014 +0200 @@ -91,6 +91,22 @@ attachment = SubjectRelation('File') +class Frozable(EntityType): + __permissions__ = { + 'read': ('managers', 'users'), + 'add': ('managers', 'users'), + 'update': ('managers', ERQLExpression('X frozen False'),), + 'delete': ('managers', ERQLExpression('X frozen False'),) + } + name = String() + frozen = Boolean(default=False, + __permissions__ = { + 'read': ('managers', 'users'), + 'add': ('managers', 'users'), + 'update': ('managers', 'owners') + }) + + class Personne(EntityType): __unique_together__ = [('nom', 'prenom', 'datenaiss')] nom = String(fulltextindexed=True, required=True, maxsize=64) diff -r b240b33c7125 -r c84ad981fc4a server/test/data/schema.py --- a/server/test/data/schema.py Tue Sep 23 17:34:36 2014 +0200 +++ b/server/test/data/schema.py Thu Sep 25 15:49:13 2014 +0200 @@ -1,4 +1,4 @@ -# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -92,6 +92,7 @@ type = String(maxsize=6) para = String(maxsize=512, __permissions__ = { + 'add': ('managers', ERQLExpression('X in_state S, S name "todo"')), 'read': ('managers', 'users', 'guests'), 'update': ('managers', ERQLExpression('X in_state S, S name "todo"')), }) @@ -109,6 +110,23 @@ 'S,Y')]) todo_by = SubjectRelation('CWUser') + +class Frozable(EntityType): + __permissions__ = { + 'read': ('managers', 'users'), + 'add': ('managers', 'users'), + 'update': ('managers', ERQLExpression('X frozen False'),), + 'delete': ('managers', ERQLExpression('X frozen False'),) + } + name = String() + frozen = Boolean(default=False, + __permissions__ = { + 'read': ('managers', 'users'), + 'add': ('managers', 'users'), + 'update': ('managers', 'owners') + }) + + class Personne(EntityType): __unique_together__ = [('nom', 'prenom', 'inline2')] nom = String(fulltextindexed=True, required=True, maxsize=64) diff -r b240b33c7125 -r c84ad981fc4a server/test/unittest_querier.py --- a/server/test/unittest_querier.py Tue Sep 23 17:34:36 2014 +0200 +++ b/server/test/unittest_querier.py Thu Sep 25 15:49:13 2014 +0200 @@ -176,8 +176,8 @@ 'X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWComputedRType, ' ' CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, ' ' CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, ' - ' Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, ' - ' Folder, Note, Old, Personne, RQLExpression, Societe, State, SubDivision, ' + ' Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, ' + ' Frozable, Note, Old, Personne, RQLExpression, Societe, State, SubDivision, ' ' SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)') self.assertListEqual(sorted(solutions), sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'}, @@ -205,6 +205,7 @@ {'X': 'ExternalUri', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'File', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Folder', 'ETN': 'String', 'ET': 'CWEType'}, + {'X': 'Frozable', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Note', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Old', 'ETN': 'String', 'ET': 'CWEType'}, {'X': 'Personne', 'ETN': 'String', 'ET': 'CWEType'}, @@ -605,16 +606,16 @@ self.assertListEqual(rset.rows, [[u'description_format', 13], [u'description', 14], - [u'name', 18], - [u'created_by', 44], - [u'creation_date', 44], - [u'cw_source', 44], - [u'cwuri', 44], - [u'in_basket', 44], - [u'is', 44], - [u'is_instance_of', 44], - [u'modification_date', 44], - [u'owned_by', 44]]) + [u'name', 19], + [u'created_by', 45], + [u'creation_date', 45], + [u'cw_source', 45], + [u'cwuri', 45], + [u'in_basket', 45], + [u'is', 45], + [u'is_instance_of', 45], + [u'modification_date', 45], + [u'owned_by', 45]]) def test_select_aggregat_having_dumb(self): # dumb but should not raise an error diff -r b240b33c7125 -r c84ad981fc4a server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Tue Sep 23 17:34:36 2014 +0200 +++ b/server/test/unittest_schemaserial.py Thu Sep 25 15:49:13 2014 +0200 @@ -327,7 +327,7 @@ 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 3, - 'defaultval': Binary('text/plain'), + 'defaultval': Binary.zpickle(u'text/plain'), 'indexed': False, 'formula': None, 'cardinality': u'?1'}), @@ -344,7 +344,6 @@ list(rdef2rql(schema['description_format'].rdefs[('CWRType', 'String')], cstrtypemap))) - def test_updateeschema2rql1(self): self.assertListEqual([('SET X description %(description)s,X final %(final)s,' 'X name %(name)s WHERE X eid %(x)s', diff -r b240b33c7125 -r c84ad981fc4a server/test/unittest_security.py --- a/server/test/unittest_security.py Tue Sep 23 17:34:36 2014 +0200 +++ b/server/test/unittest_security.py Thu Sep 25 15:49:13 2014 +0200 @@ -423,6 +423,21 @@ self.assertRaises(Unauthorized, cnx.commit) cnx.execute('SET X web "http://www.logilab.org" WHERE X eid %(x)s', {'x': eid}) cnx.commit() + with self.new_access('iaminusersgrouponly').repo_cnx() as cnx: + cnx.execute('INSERT Frozable F: F name "Foo"') + cnx.commit() + cnx.execute('SET F name "Bar" WHERE F is Frozable') + cnx.commit() + cnx.execute('SET F name "BaBar" WHERE F is Frozable') + cnx.execute('SET F frozen True WHERE F is Frozable') + with self.assertRaises(Unauthorized): + cnx.commit() + cnx.rollback() + cnx.execute('SET F frozen True WHERE F is Frozable') + cnx.commit() + cnx.execute('SET F name "Bar" WHERE F is Frozable') + with self.assertRaises(Unauthorized): + cnx.commit() def test_attribute_security_rqlexpr(self): with self.admin_access.repo_cnx() as cnx: diff -r b240b33c7125 -r c84ad981fc4a server/test/unittest_session.py --- a/server/test/unittest_session.py Tue Sep 23 17:34:36 2014 +0200 +++ b/server/test/unittest_session.py Thu Sep 25 15:49:13 2014 +0200 @@ -18,6 +18,8 @@ from cubicweb.devtools.testlib import CubicWebTC from cubicweb.server.session import HOOKS_ALLOW_ALL, HOOKS_DENY_ALL +from cubicweb.server import hook +from cubicweb.predicates import is_instance class InternalSessionTC(CubicWebTC): def test_dbapi_query(self): @@ -76,7 +78,7 @@ self.assertEqual(set(), session.disabled_hook_categories) self.assertEqual(set(), session.enabled_hook_categories) - def test_explicite_connection(self): + def test_explicit_connection(self): with self.session.new_cnx() as cnx: rset = cnx.execute('Any X LIMIT 1 WHERE X is CWUser') self.assertEqual(1, len(rset)) @@ -98,7 +100,24 @@ self.assertIsNotNone(new_user.login) self.assertFalse(cnx._open) - + def test_connection_exit(self): + """exiting a connection should roll back the transaction, including any + pending operations""" + self.rollbacked = False + class RollbackOp(hook.Operation): + _test = self + def rollback_event(self): + self._test.rollbacked = True + class RollbackHook(hook.Hook): + __regid__ = 'rollback' + events = ('after_update_entity',) + __select__ = hook.Hook.__select__ & is_instance('CWGroup') + def __call__(self): + RollbackOp(self._cw) + with self.temporary_appobjects(RollbackHook): + with self.admin_access.client_cnx() as cnx: + cnx.execute('SET G name "foo" WHERE G is CWGroup, G name "managers"') + self.assertTrue(self.rollbacked) if __name__ == '__main__': from logilab.common.testlib import unittest_main diff -r b240b33c7125 -r c84ad981fc4a test/data/schema.py --- a/test/data/schema.py Tue Sep 23 17:34:36 2014 +0200 +++ b/test/data/schema.py Thu Sep 25 15:49:13 2014 +0200 @@ -89,3 +89,8 @@ class StateFull(WorkflowableEntityType): name = String() + + +class Reference(EntityType): + nom = String(unique=True) + ean = String(unique=True, required=True) diff -r b240b33c7125 -r c84ad981fc4a test/unittest_entity.py --- a/test/unittest_entity.py Tue Sep 23 17:34:36 2014 +0200 +++ b/test/unittest_entity.py Thu Sep 25 15:49:13 2014 +0200 @@ -754,6 +754,11 @@ # unique attr with None value (nom in this case) friend = req.create_entity('Ami', prenom=u'bob') self.assertEqual(friend.rest_path(), unicode(friend.eid)) + # 'ref' below is created without the unique but not required + # attribute, make sur that the unique _and_ required 'ean' is used + # as the rest attribute + ref = req.create_entity('Reference', ean=u'42-1337-42') + self.assertEqual(ref.rest_path(), 'reference/42-1337-42') def test_can_use_rest_path(self): self.assertTrue(can_use_rest_path(u'zobi')) diff -r b240b33c7125 -r c84ad981fc4a test/unittest_schema.py --- a/test/unittest_schema.py Tue Sep 23 17:34:36 2014 +0200 +++ b/test/unittest_schema.py Thu Sep 25 15:49:13 2014 +0200 @@ -171,7 +171,7 @@ 'CWUniqueTogetherConstraint', 'CWUser', 'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note', 'Password', 'Personne', 'Produit', - 'RQLExpression', + 'RQLExpression', 'Reference', 'Service', 'Societe', 'State', 'StateFull', 'String', 'SubNote', 'SubWorkflowExitPoint', 'Tag', 'TZDatetime', 'TZTime', 'Time', 'Transition', 'TrInfo', 'Usine', @@ -191,7 +191,7 @@ 'data', 'data_encoding', 'data_format', 'data_name', 'default_workflow', 'defaultval', 'delete_permission', 'description', 'description_format', 'destination_state', 'dirige', - 'ecrit_par', 'eid', 'end_timestamp', 'evaluee', 'expression', 'exprtype', 'extra_props', + 'ean', 'ecrit_par', 'eid', 'end_timestamp', 'evaluee', 'expression', 'exprtype', 'extra_props', 'fabrique_par', 'final', 'firstname', 'for_user', 'formula', 'fournit', 'from_entity', 'from_state', 'fulltext_container', 'fulltextindexed', @@ -503,6 +503,7 @@ ('cw_source', 'Personne', 'CWSource', 'object'), ('cw_source', 'Produit', 'CWSource', 'object'), ('cw_source', 'RQLExpression', 'CWSource', 'object'), + ('cw_source', 'Reference', 'CWSource', 'object'), ('cw_source', 'Service', 'CWSource', 'object'), ('cw_source', 'Societe', 'CWSource', 'object'), ('cw_source', 'State', 'CWSource', 'object'), diff -r b240b33c7125 -r c84ad981fc4a toolsutils.py diff -r b240b33c7125 -r c84ad981fc4a uilib.py --- a/uilib.py Tue Sep 23 17:34:36 2014 +0200 +++ b/uilib.py Thu Sep 25 15:49:13 2014 +0200 @@ -444,12 +444,14 @@ def exc_message(ex, encoding): try: - return unicode(ex) + excmsg = unicode(ex) except Exception: try: - return unicode(str(ex), encoding, 'replace') + excmsg = unicode(str(ex), encoding, 'replace') except Exception: - return unicode(repr(ex), encoding, 'replace') + excmsg = unicode(repr(ex), encoding, 'replace') + exctype = unicode(ex.__class__.__name__) + return u'%s: %s' % (exctype, excmsg) def rest_traceback(info, exception): diff -r b240b33c7125 -r c84ad981fc4a view.py --- a/view.py Tue Sep 23 17:34:36 2014 +0200 +++ b/view.py Thu Sep 25 15:49:13 2014 +0200 @@ -536,14 +536,6 @@ __registry__ = 'adapters' -class auto_unwrap_bw_compat(type): - def __new__(mcs, name, bases, classdict): - cls = type.__new__(mcs, name, bases, classdict) - if not classdict.get('__needs_bw_compat__'): - unwrap_adapter_compat(cls) - return cls - - class EntityAdapter(Adapter): """base class for entity adapters (eg adapt an entity to an interface)""" def __init__(self, _cw, **kwargs): diff -r b240b33c7125 -r c84ad981fc4a web/_exceptions.py --- a/web/_exceptions.py Tue Sep 23 17:34:36 2014 +0200 +++ b/web/_exceptions.py Thu Sep 25 15:49:13 2014 +0200 @@ -101,7 +101,7 @@ """raised when a json remote call fails """ def __init__(self, reason='', status=httplib.INTERNAL_SERVER_ERROR): - super(RemoteCallFailed, self).__init__(status=status) + super(RemoteCallFailed, self).__init__(reason, status=status) self.reason = reason def dumps(self): diff -r b240b33c7125 -r c84ad981fc4a web/component.py --- a/web/component.py Tue Sep 23 17:34:36 2014 +0200 +++ b/web/component.py Thu Sep 25 15:49:13 2014 +0200 @@ -513,12 +513,12 @@ """builds HTML link to edit relation between `entity` and `etarget`""" args = {role(self) : entity.eid, target(self): etarget.eid} # for each target, provide a link to edit the relation - jscall = unicode(js.cw.utils.callAddOrDeleteThenReload(fname, - self.rtype, - args['subject'], - args['object'])) + jscall = js.cw.utils.callAjaxFuncThenReload(fname, + self.rtype, + args['subject'], + args['object']) return u'[%s] %s' % ( - xml_escape(jscall), label, etarget.view('incontext')) + xml_escape(unicode(jscall)), label, etarget.view('incontext')) def related_boxitems(self, entity): return [self.box_item(entity, etarget, 'delete_relation', u'-') diff -r b240b33c7125 -r c84ad981fc4a web/data/cubicweb.form.css --- a/web/data/cubicweb.form.css Tue Sep 23 17:34:36 2014 +0200 +++ b/web/data/cubicweb.form.css Thu Sep 25 15:49:13 2014 +0200 @@ -164,7 +164,7 @@ font-weight: bold; } -div.pendingDelete { +.pendingDelete { text-decoration: line-through; } diff -r b240b33c7125 -r c84ad981fc4a web/data/cubicweb.js --- a/web/data/cubicweb.js Tue Sep 23 17:34:36 2014 +0200 +++ b/web/data/cubicweb.js Thu Sep 25 15:49:13 2014 +0200 @@ -339,14 +339,14 @@ ); }, - callAddOrDeleteThenReload: function (add_or_delete, rtype, subjeid, objeid) { - var d = asyncRemoteExec(add_or_delete, rtype, subjeid, objeid); - d.addCallback(function() { + callAjaxFuncThenReload: function callAjaxFuncThenReload (/*...*/) { + var d = asyncRemoteExec.apply(null, arguments); + d.addCallback(function(msg) { window.location.reload(); + if (msg) + updateMessage(msg); }); } - - }); /** DOM factories ************************************************************/ diff -r b240b33c7125 -r c84ad981fc4a web/facet.py --- a/web/facet.py Tue Sep 23 17:34:36 2014 +0200 +++ b/web/facet.py Thu Sep 25 15:49:13 2014 +0200 @@ -1620,9 +1620,11 @@ cssclass += ' hideFacetBody' w(u'
%s
\n' % (cssclass, xml_escape(self.facet.__regid__), title)) + w(u'
\n') w(u'\n' % ( xml_escape(self.facet.__regid__), self.value or u'')) w(u'
\n') + w(u'\n') class FacetRangeWidget(htmlwidgets.HTMLWidget): diff -r b240b33c7125 -r c84ad981fc4a web/http_headers.py --- a/web/http_headers.py Tue Sep 23 17:34:36 2014 +0200 +++ b/web/http_headers.py Thu Sep 25 15:49:13 2014 +0200 @@ -1567,7 +1567,7 @@ 'Server': (unique, str, singleHeader), 'Set-Cookie': (generateSetCookie,), 'Set-Cookie2': (generateSetCookie2,), - 'Vary': (generateList, singleHeader), + 'Vary': (set, generateList, singleHeader), 'WWW-Authenticate': (generateWWWAuthenticate,) } diff -r b240b33c7125 -r c84ad981fc4a web/test/data/schema.py --- a/web/test/data/schema.py Tue Sep 23 17:34:36 2014 +0200 +++ b/web/test/data/schema.py Thu Sep 25 15:49:13 2014 +0200 @@ -43,7 +43,11 @@ class Personne(EntityType): nom = String(fulltextindexed=True, required=True, maxsize=64) prenom = String(fulltextindexed=True, maxsize=64) - sexe = String(maxsize=1, default='M') + sexe = String(maxsize=1, default='M', + __permissions__={ + 'read': ('managers', 'users', 'guests',), + 'add': ('managers', 'users'), + 'update': ('managers', )}) promo = String(vocabulary=('bon','pasbon')) titre = String(fulltextindexed=True, maxsize=128) ass = String(maxsize=128) diff -r b240b33c7125 -r c84ad981fc4a web/test/unittest_form.py diff -r b240b33c7125 -r c84ad981fc4a web/test/unittest_views_csv.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/test/unittest_views_csv.py Thu Sep 25 15:49:13 2014 +0200 @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# 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 cubicweb.devtools.testlib import CubicWebTC + + +class CSVExportViewsTC(CubicWebTC): + + def test_csvexport(self): + with self.admin_access.web_request() as req: + rset = req.execute('Any GN,COUNT(X) GROUPBY GN ORDERBY GN ' + 'WHERE X in_group G, G name GN') + data = self.view('csvexport', rset, req=req) + self.assertEqual(req.headers_out.getRawHeaders('content-type'), + ['text/comma-separated-values;charset=UTF-8']) + expected_data = "String;COUNT(CWUser)\nguests;1\nmanagers;1" + self.assertMultiLineEqual(expected_data, data) + + def test_csvexport_on_empty_rset(self): + """Should return the CSV header. + """ + with self.admin_access.web_request() as req: + rset = req.execute('Any GN,COUNT(X) GROUPBY GN ORDERBY GN ' + 'WHERE X in_group G, G name GN, X login "Miles"') + data = self.view('csvexport', rset, req=req) + self.assertEqual(req.headers_out.getRawHeaders('content-type'), + ['text/comma-separated-values;charset=UTF-8']) + expected_data = "String;COUNT(CWUser)" + self.assertMultiLineEqual(expected_data, data) + + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff -r b240b33c7125 -r c84ad981fc4a web/test/unittest_views_editforms.py --- a/web/test/unittest_views_editforms.py Tue Sep 23 17:34:36 2014 +0200 +++ b/web/test/unittest_views_editforms.py Thu Sep 25 15:49:13 2014 +0200 @@ -148,6 +148,26 @@ self.vreg['forms'].select('edition', req, entity=rset.get_entity(0, 0)) self.assertFalse(any(f for f in form.fields if f is None)) + def test_attribute_add_permissions(self): + # https://www.cubicweb.org/ticket/4342844 + with self.admin_access.repo_cnx() as cnx: + self.create_user(cnx, 'toto') + cnx.commit() + with self.new_access('toto').web_request() as req: + e = self.vreg['etypes'].etype_class('Personne')(req) + cform = self.vreg['forms'].select('edition', req, entity=e) + self.assertIn('sexe', + [rschema.type + for rschema, _ in cform.editable_attributes()]) + with self.new_access('toto').repo_cnx() as cnx: + person_eid = cnx.create_entity('Personne', nom=u'Robert').eid + cnx.commit() + person = req.entity_from_eid(person_eid) + mform = self.vreg['forms'].select('edition', req, entity=person) + self.assertNotIn('sexe', + [rschema.type + for rschema, _ in mform.editable_attributes()]) + class FormViewsTC(CubicWebTC): diff -r b240b33c7125 -r c84ad981fc4a web/test/unittest_viewselector.py --- a/web/test/unittest_viewselector.py Tue Sep 23 17:34:36 2014 +0200 +++ b/web/test/unittest_viewselector.py Thu Sep 25 15:49:13 2014 +0200 @@ -107,8 +107,11 @@ def test_possible_views_noresult(self): with self.admin_access.web_request() as req: rset = req.execute('Any X WHERE X eid 999999') - self.assertListEqual([('jsonexport', json.JsonRsetView)], - self.pviews(req, rset)) + self.assertListEqual(self.pviews(req, rset), + [('csvexport', csvexport.CSVRsetView), + ('ecsvexport', csvexport.CSVEntityView), + ('jsonexport', json.JsonRsetView), + ]) def test_possible_views_one_egroup(self): with self.admin_access.web_request() as req: @@ -214,6 +217,7 @@ rset = req.execute('Any N, X WHERE X in_group Y, Y name N') self.assertListEqual(self.pviews(req, rset), [('csvexport', csvexport.CSVRsetView), + ('ecsvexport', csvexport.CSVEntityView), ('jsonexport', json.JsonRsetView), ('rsetxml', xmlrss.XMLRsetView), ('table', tableview.RsetTableView), diff -r b240b33c7125 -r c84ad981fc4a web/views/autoform.py --- a/web/views/autoform.py Tue Sep 23 17:34:36 2014 +0200 +++ b/web/views/autoform.py Thu Sep 25 15:49:13 2014 +0200 @@ -844,9 +844,9 @@ return [(schema[rtype], role) for rtype, role in self.display_fields] if self.edited_entity.has_eid() and not self.edited_entity.cw_has_perm('update'): return [] - # XXX we should simply put eid in the generated section, no? + action = 'update' if self.edited_entity.has_eid() else 'add' return [(rtype, role) for rtype, _, role in self._relations_by_section( - 'attributes', 'update', strict)] + 'attributes', action, strict)] def editable_relations(self): """return a sorted list of (relation's label, relation'schema, role) for @@ -988,7 +988,7 @@ _AFS = uicfg.autoform_section # use primary and not generated for eid since it has to be an hidden -_AFS.tag_attribute(('*', 'eid'), 'main', 'attributes') +_AFS.tag_attribute(('*', 'eid'), 'main', 'hidden') _AFS.tag_attribute(('*', 'eid'), 'muledit', 'attributes') _AFS.tag_attribute(('*', 'description'), 'main', 'attributes') _AFS.tag_attribute(('*', 'has_text'), 'main', 'hidden') diff -r b240b33c7125 -r c84ad981fc4a web/views/csvexport.py --- a/web/views/csvexport.py Tue Sep 23 17:34:36 2014 +0200 +++ b/web/views/csvexport.py Thu Sep 25 15:49:13 2014 +0200 @@ -21,6 +21,7 @@ _ = unicode from cubicweb.schema import display_name +from cubicweb.predicates import any_rset from cubicweb.uilib import UnicodeCSVWriter from cubicweb.view import EntityView, AnyRsetView @@ -47,6 +48,7 @@ class CSVRsetView(CSVMixIn, AnyRsetView): """dumps raw result set in CSV""" __regid__ = 'csvexport' + __select__ = any_rset() title = _('csv export') def call(self): @@ -78,6 +80,7 @@ contents) """ __regid__ = 'ecsvexport' + __select__ = any_rset() title = _('csv export (entities)') def call(self): diff -r b240b33c7125 -r c84ad981fc4a web/views/forms.py diff -r b240b33c7125 -r c84ad981fc4a web/views/uicfg.py --- a/web/views/uicfg.py Tue Sep 23 17:34:36 2014 +0200 +++ b/web/views/uicfg.py Thu Sep 25 15:49:13 2014 +0200 @@ -330,7 +330,7 @@ assert section in ('attributes', 'metadata', 'hidden') relpermission = 'add' else: - assert section not in ('attributes', 'metadata', 'hidden') + assert section not in ('metadata', 'hidden') relpermission = permission for rschema, targetschemas, role in eschema.relation_definitions(True): _targetschemas = [] diff -r b240b33c7125 -r c84ad981fc4a wsgi/request.py --- a/wsgi/request.py Tue Sep 23 17:34:36 2014 +0200 +++ b/wsgi/request.py Thu Sep 25 15:49:13 2014 +0200 @@ -25,9 +25,12 @@ __docformat__ = "restructuredtext en" +import tempfile + from StringIO import StringIO from urllib import quote from urlparse import parse_qs +from warnings import warn from cubicweb.multipart import copy_file, parse_form_data from cubicweb.web.request import CubicWebRequestBase @@ -39,6 +42,10 @@ """ def __init__(self, environ, vreg): + # self.vreg is used in get_posted_data, which is called before the + # parent constructor. + self.vreg = vreg + self.environ = environ self.path = environ['PATH_INFO'] self.method = environ['REQUEST_METHOD'].upper() @@ -59,11 +66,19 @@ headers_in = dict((normalize_header(k[5:]), v) for k, v in self.environ.items() if k.startswith('HTTP_')) + if 'CONTENT_TYPE' in environ: + headers_in['Content-Type'] = environ['CONTENT_TYPE'] https = self.is_secure() + if self.path.startswith('/https/'): + self.path = self.path[6:] + self.environ['PATH_INFO'] = self.path + https = True + post, files = self.get_posted_data() super(CubicWebWsgiRequest, self).__init__(vreg, https, post, headers= headers_in) + self.content = environ['wsgi.input'] if files is not None: for key, part in files.iteritems(): name = None @@ -114,6 +129,38 @@ if self.method == 'POST': forms, files = parse_form_data(self.environ, strict=True, mem_limit=self.vreg.config['max-post-length']) - post.update(forms) + post.update(forms.dict) self.content.seek(0, 0) return post, files + + def setup_params(self, params): + # This is a copy of CubicWebRequestBase.setup_params, but without + # converting unicode strings because it is partially done by + # get_posted_data + self.form = {} + if params is None: + return + encoding = self.encoding + for param, val in params.iteritems(): + if isinstance(val, (tuple, list)): + val = [ + unicode(x, encoding) if isinstance(x, str) else x + for x in val] + if len(val) == 1: + val = val[0] + elif isinstance(val, str): + val = unicode(val, encoding) + if param in self.no_script_form_params and val: + val = self.no_script_form_param(param, val) + if param == '_cwmsgid': + self.set_message_id(val) + elif param == '__message': + warn('[3.13] __message in request parameter is deprecated (may ' + 'only be given to .build_url). Seeing this message usualy ' + 'means your application hold some
where you should ' + 'replace use of __message hidden input by form.set_message, ' + 'so new _cwmsgid mechanism is properly used', + DeprecationWarning) + self.set_message(val) + else: + self.form[param] = val diff -r b240b33c7125 -r c84ad981fc4a wsgi/test/unittest_wsgi.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wsgi/test/unittest_wsgi.py Thu Sep 25 15:49:13 2014 +0200 @@ -0,0 +1,92 @@ +# encoding=utf-8 + +import webtest.app +from StringIO import StringIO + +from cubicweb.devtools.webtest import CubicWebTestTC + +from cubicweb.wsgi.request import CubicWebWsgiRequest + + +class WSGIAppTC(CubicWebTestTC): + def test_content_type(self): + r = webtest.app.TestRequest.blank('/', {'CONTENT_TYPE': 'text/plain'}) + + req = CubicWebWsgiRequest(r.environ, self.vreg) + + self.assertEqual('text/plain', req.get_header('Content-Type')) + + def test_content_body(self): + r = webtest.app.TestRequest.blank('/', { + 'CONTENT_LENGTH': 12, + 'CONTENT_TYPE': 'text/plain', + 'wsgi.input': StringIO('some content')}) + + req = CubicWebWsgiRequest(r.environ, self.vreg) + + self.assertEqual('some content', req.content.read()) + + def test_http_scheme(self): + r = webtest.app.TestRequest.blank('/', { + 'wsgi.url_scheme': 'http'}) + + req = CubicWebWsgiRequest(r.environ, self.vreg) + + self.assertFalse(req.https) + + def test_https_scheme(self): + r = webtest.app.TestRequest.blank('/', { + 'wsgi.url_scheme': 'https'}) + + req = CubicWebWsgiRequest(r.environ, self.vreg) + + self.assertTrue(req.https) + + def test_https_prefix(self): + r = webtest.app.TestRequest.blank('/https/', { + 'wsgi.url_scheme': 'http'}) + + req = CubicWebWsgiRequest(r.environ, self.vreg) + + self.assertTrue(req.https) + + def test_big_content(self): + content = 'x'*100001 + r = webtest.app.TestRequest.blank('/', { + 'CONTENT_LENGTH': len(content), + 'CONTENT_TYPE': 'text/plain', + 'wsgi.input': StringIO(content)}) + + req = CubicWebWsgiRequest(r.environ, self.vreg) + + self.assertEqual(content, req.content.read()) + + def test_post(self): + self.webapp.post( + '/', + params={'__login': self.admlogin, '__password': self.admpassword}) + + def test_get_multiple_variables(self): + r = webtest.app.TestRequest.blank('/?arg=1&arg=2') + req = CubicWebWsgiRequest(r.environ, self.vreg) + + self.assertEqual([u'1', u'2'], req.form['arg']) + + def test_post_multiple_variables(self): + r = webtest.app.TestRequest.blank('/', POST='arg=1&arg=2') + req = CubicWebWsgiRequest(r.environ, self.vreg) + + self.assertEqual([u'1', u'2'], req.form['arg']) + + def test_post_unicode_urlencoded(self): + params = 'arg=%C3%A9' + r = webtest.app.TestRequest.blank( + '/', POST=params, content_type='application/x-www-form-urlencoded') + req = CubicWebWsgiRequest(r.environ, self.vreg) + self.assertEqual(u"é", req.form['arg']) + + @classmethod + def init_config(cls, config): + super(WSGIAppTC, cls).init_config(config) + config.https_uiprops = None + config.https_datadir_url = None