# HG changeset patch # User Sylvain Thénault # Date 1486125452 -3600 # Node ID 3c58ea2fd745b00022f6ff52aee3cb952c7860e2 # Parent 8de62610cea26404646e01b0cdd958f1c616b524# Parent f96f77a190f2121793081363268250d8a113f442 Merge 3.24.5 into default branch diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb.spec --- a/cubicweb.spec Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb.spec Fri Feb 03 13:37:32 2017 +0100 @@ -27,7 +27,7 @@ Requires: %{python}-rql >= 0.34.0 Requires: %{python}-yams >= 0.44.0 Requires: %{python}-logilab-database >= 1.15.0 -Requires: %{python}-passlib < 2.0 +Requires: %{python}-passlib => 1.7.0 Requires: %{python}-lxml Requires: %{python}-twisted-web < 16.0.0 Requires: %{python}-markdown diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/__init__.py --- a/cubicweb/__init__.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/__init__.py Fri Feb 03 13:37:32 2017 +0100 @@ -26,6 +26,7 @@ import pickle import pkgutil import sys +import types import warnings import zlib @@ -294,7 +295,9 @@ sys.meta_path.append(self) def find_module(self, fullname, path=None): - if fullname.startswith('cubes.'): + if fullname == 'cubes': + return self + elif fullname.startswith('cubes.'): modname = 'cubicweb_' + fullname.split('.', 1)[1] try: modinfo = imp.find_module(modname) @@ -302,3 +305,9 @@ return None else: return pkgutil.ImpLoader(fullname, *modinfo) + + def load_module(self, fullname): + if fullname != 'cubes': + raise ImportError('No module named {0}'.format(fullname)) + mod = sys.modules[fullname] = types.ModuleType(fullname, doc='CubicWeb cubes') + return mod diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/__pkginfo__.py --- a/cubicweb/__pkginfo__.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/__pkginfo__.py Fri Feb 03 13:37:32 2017 +0100 @@ -27,8 +27,8 @@ modname = distname = "cubicweb" -numversion = (3, 24, 5) -version = '.'.join(str(num) for num in numversion) +numversion = (3, 25, 0) +version = '.'.join(str(num) for num in numversion) + '.dev0' description = "a repository of entities / relations for knowledge management" author = "Logilab" diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/cwconfig.py --- a/cubicweb/cwconfig.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/cwconfig.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -177,16 +177,15 @@ Directory where pid files will be written """ + from __future__ import print_function - - import importlib import logging import logging.config import os -from os.path import (exists, join, expanduser, abspath, normpath, realpath, - basename, isdir, dirname, splitext) +from os.path import (exists, join, expanduser, abspath, normpath, + basename, isdir, dirname, splitext, realpath) import pkgutil import pkg_resources import re @@ -194,7 +193,7 @@ import stat import sys from threading import Lock -from warnings import warn, filterwarnings +from warnings import filterwarnings from six import text_type @@ -221,6 +220,7 @@ except IndexError: raise ConfigurationError('no such config %r (check it exists with "cubicweb-ctl list")' % name) + def possible_configurations(directory): """return a list of installed configurations in a directory according to \*-ctl files @@ -228,6 +228,7 @@ return [name for name in ('repository', 'all-in-one') if exists(join(directory, '%s.conf' % name))] + def guess_configuration(directory): """try to guess the configuration to use for a directory. If multiple configurations are found, ConfigurationError is raised @@ -238,6 +239,7 @@ % (directory, modes)) return modes[0] + def _find_prefix(start_path=None): """Return the prefix path of CubicWeb installation. @@ -274,6 +276,40 @@ return cube +def _expand_modname(modname): + """expand modules names `modname` if exists by walking non package submodules + and yield (submodname, filepath) including `modname` itself + + If the file ends with .pyc or .pyo (python bytecode) also check that the + corresponding source .py file exists before yielding. + """ + try: + loader = pkgutil.find_loader(modname) + except ImportError: + return + if not loader: + return + + def check_source_file(filepath): + if filepath[-4:] in ('.pyc', '.pyo'): + if not exists(filepath[:-1]): + return False + return True + + filepath = loader.get_filename() + if not check_source_file(filepath): + return + yield modname, filepath + if loader.is_package(modname): + path = dirname(filepath) + for subloader, subname, ispkg in pkgutil.walk_packages([path]): + # ignore subpackages (historical behavior) + if not ispkg: + filepath = subloader.find_module(subname).get_filename() + if check_source_file(filepath): + yield modname + '.' + subname, filepath + + # persistent options definition PERSISTENT_OPTIONS = ( ('encoding', @@ -667,14 +703,8 @@ """update python path if necessary""" from cubicweb import _CubesImporter _CubesImporter.install() - cubes_parent_dir = normpath(join(cls.CUBES_DIR, '..')) - if not cubes_parent_dir in sys.path: - sys.path.insert(0, cubes_parent_dir) - try: - import cubes - cubes.__path__ = cls.cubes_search_path() - except ImportError: - return # cubes dir doesn't exists + import cubes + cubes.__path__ = cls.cubes_search_path() @classmethod def load_available_configs(cls): @@ -692,7 +722,9 @@ 'devtools.devctl', 'pyramid.pyramidctl'): try: __import__('cubicweb.%s' % ctlmod) - except ImportError: + except ImportError as exc: + cls.warning('failed to load cubicweb-ctl plugin %s (%s)', + ctlmod, exc) continue cls.info('loaded cubicweb-ctl plugin %s', ctlmod) for cube in cls.available_cubes(): @@ -778,57 +810,26 @@ # configure simpleTal logger logging.getLogger('simpleTAL').setLevel(logging.ERROR) - def appobjects_path(self): - """return a list of files or directories where the registry will look - for application objects. By default return nothing in NoApp config. + def schema_modnames(self): + modnames = [] + for name in ('bootstrap', 'base', 'workflow', 'Bookmark'): + modnames.append(('cubicweb', 'cubicweb.schemas.' + name)) + for cube in reversed(self.cubes()): + for modname, filepath in _expand_modname('cubes.{0}.schema'.format(cube)): + modnames.append((cube, modname)) + if self.apphome: + apphome = realpath(self.apphome) + for modname, filepath in _expand_modname('schema'): + if realpath(filepath).startswith(apphome): + modnames.append(('data', modname)) + return modnames + + def appobjects_modnames(self): + """return a list of modules where the registry will look for + application objects. By default return nothing in NoApp config. """ return [] - def build_appobjects_path(self, templpath, evobjpath=None, tvobjpath=None): - """given a list of directories, return a list of sub files and - directories that should be loaded by the instance objects registry. - - :param evobjpath: - optional list of sub-directories (or files without the .py ext) of - the cubicweb library that should be tested and added to the output list - if they exists. If not give, default to `cubicweb_appobject_path` class - attribute. - :param tvobjpath: - optional list of sub-directories (or files without the .py ext) of - directories given in `templpath` that should be tested and added to - the output list if they exists. If not give, default to - `cube_appobject_path` class attribute. - """ - vregpath = self.build_appobjects_cubicweb_path(evobjpath) - vregpath += self.build_appobjects_cube_path(templpath, tvobjpath) - return vregpath - - def build_appobjects_cubicweb_path(self, evobjpath=None): - vregpath = [] - if evobjpath is None: - evobjpath = self.cubicweb_appobject_path - # NOTE: for the order, see http://www.cubicweb.org/ticket/2330799 - # it is clearly a workaround - for subdir in sorted(evobjpath, key=lambda x:x != 'entities'): - path = join(CW_SOFTWARE_ROOT, subdir) - if exists(path): - vregpath.append(path) - return vregpath - - def build_appobjects_cube_path(self, templpath, tvobjpath=None): - vregpath = [] - if tvobjpath is None: - tvobjpath = self.cube_appobject_path - for directory in templpath: - # NOTE: for the order, see http://www.cubicweb.org/ticket/2330799 - for subdir in sorted(tvobjpath, key=lambda x:x != 'entities'): - path = join(directory, subdir) - if exists(path): - vregpath.append(path) - elif exists(path + '.py'): - vregpath.append(path + '.py') - return vregpath - apphome = None def load_site_cubicweb(self, cubes=()): @@ -1309,21 +1310,49 @@ try: tr = translation('cubicweb', path, languages=[language]) self.translations[language] = (tr.ugettext, tr.upgettext) - except (ImportError, AttributeError, IOError): + except IOError: if self.mode != 'test': # in test contexts, data/i18n does not exist, hence # logging will only pollute the logs self.exception('localisation support error for language %s', language) - def appobjects_path(self): - """return a list of files or directories where the registry will look - for application objects - """ - templpath = list(reversed(self.cubes_path())) - if self.apphome: # may be unset in tests - templpath.append(self.apphome) - return self.build_appobjects_path(templpath) + @staticmethod + def _sorted_appobjects(appobjects): + appobjects = sorted(appobjects) + try: + index = appobjects.index('entities') + except ValueError: + pass + else: + # put entities first + appobjects.insert(0, appobjects.pop(index)) + return appobjects + + def appobjects_cube_modnames(self, cube): + modnames = [] + cube_submodnames = self._sorted_appobjects(self.cube_appobject_path) + for name in cube_submodnames: + for modname, filepath in _expand_modname('.'.join(['cubes', cube, name])): + modnames.append(modname) + return modnames + + def appobjects_modnames(self): + modnames = [] + for name in self._sorted_appobjects(self.cubicweb_appobject_path): + for modname, filepath in _expand_modname('cubicweb.' + name): + modnames.append(modname) + for cube in reversed(self.cubes()): + modnames.extend(self.appobjects_cube_modnames(cube)) + if self.apphome: + cube_submodnames = self._sorted_appobjects(self.cube_appobject_path) + apphome = realpath(self.apphome) + for name in cube_submodnames: + for modname, filepath in _expand_modname(name): + # ensure file is in apphome + if realpath(filepath).startswith(apphome): + modnames.append(modname) + return modnames def set_sources_mode(self, sources): if not 'all' in sources: diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/cwctl.py --- a/cubicweb/cwctl.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/cwctl.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -20,15 +20,13 @@ """ from __future__ import print_function - - # *ctl module should limit the number of import to be imported as quickly as # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash # completion). So import locally in command helpers. import sys from warnings import warn, filterwarnings from os import remove, listdir, system, pathsep -from os.path import exists, join, isfile, isdir, dirname, abspath +from os.path import exists, join, isdir, dirname, abspath try: from os import kill, getpgid @@ -43,7 +41,7 @@ from logilab.common.clcommands import CommandLine from logilab.common.shellutils import ASK from logilab.common.configuration import merge_options -from logilab.common.deprecation import deprecated +from logilab.common.decorators import clear_cache from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage from cubicweb.cwconfig import CubicWebConfiguration as cwcfg, CWDEV, CONFIGURATIONS @@ -54,6 +52,7 @@ CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.', version=version, check_duplicated_command=False) + def wait_process_end(pid, maxtry=10, waittime=1): """wait for a process to actually die""" import signal @@ -62,19 +61,21 @@ while nbtry < maxtry: try: kill(pid, signal.SIGUSR1) - except (OSError, AttributeError): # XXX win32 + except (OSError, AttributeError): # XXX win32 break nbtry += 1 sleep(waittime) else: raise ExecutionError('can\'t kill process %s' % pid) + def list_instances(regdir): if isdir(regdir): return sorted(idir for idir in listdir(regdir) if isdir(join(regdir, idir))) else: return [] + def detect_available_modes(templdir): modes = [] for fname in ('schema', 'schema.py'): @@ -103,13 +104,6 @@ ) actionverb = None - @deprecated('[3.22] startorder is not used any more') - def ordered_instances(self): - """return list of known instances - """ - regdir = cwcfg.instances_dir() - return list_instances(regdir) - def run(self, args): """run the _method on each argument (a list of instance identifiers) @@ -756,6 +750,7 @@ with mih.cnx: with mih.cnx.security_enabled(False, False): mih.migrate(vcconf, reversed(toupgrade), self.config) + clear_cache(config, 'instance_md5_version') else: print('-> no data migration needed for instance %s.' % appid) # rewrite main configuration file diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/cwvreg.py --- a/cubicweb/cwvreg.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/cwvreg.py Fri Feb 03 13:37:32 2017 +0100 @@ -29,7 +29,7 @@ from logilab.common.decorators import cached, clear_cache from logilab.common.deprecation import class_deprecated -from logilab.common.modutils import cleanup_sys_modules +from logilab.common.modutils import clean_sys_modules from logilab.common.registry import RegistryStore, Registry, ObjectNotFound, RegistryNotFound from rql import RQLHelper @@ -417,7 +417,7 @@ """set instance'schema and load application objects""" self._set_schema(schema) # now we can load application's web objects - self.reload(self.config.appobjects_path(), force_reload=False) + self.reload(self.config.appobjects_modnames(), force_reload=False) # map lowered entity type names to their actual name self.case_insensitive_etypes = {} for eschema in self.schema.entities(): @@ -426,13 +426,28 @@ clear_cache(eschema, 'ordered_relations') clear_cache(eschema, 'meta_attributes') + def is_reload_needed(self, modnames): + """overriden to handle modules names instead of directories""" + lastmodifs = self._lastmodifs + for modname in modnames: + if modname not in sys.modules: + # new module to load + return True + filepath = sys.modules[modname].__file__ + if filepath.endswith('.py'): + mdate = self._mdate(filepath) + if filepath not in lastmodifs or lastmodifs[filepath] < mdate: + self.info('File %s changed since last visit', filepath) + return True + return False + def reload_if_needed(self): - path = self.config.appobjects_path() - if self.is_reload_needed(path): - self.reload(path) + modnames = self.config.appobjects_modnames() + if self.is_reload_needed(modnames): + self.reload(modnames) - def _cleanup_sys_modules(self, path): - """Remove submodules of `directories` from `sys.modules` and cleanup + def _cleanup_sys_modules(self, modnames): + """Remove modules and submodules of `modnames` from `sys.modules` and cleanup CW_EVENT_MANAGER accordingly. We take care to properly remove obsolete registry callbacks. @@ -446,18 +461,18 @@ # for non-function callable, we do nothing interesting module = getattr(func, '__module__', None) caches[id(callback)] = module - deleted_modules = set(cleanup_sys_modules(path)) + deleted_modules = set(clean_sys_modules(modnames)) 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): + def reload(self, modnames, force_reload=True): """modification detected, reset and reload the vreg""" CW_EVENT_MANAGER.emit('before-registry-reload') if force_reload: - self._cleanup_sys_modules(path) + self._cleanup_sys_modules(modnames) 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 @@ -465,9 +480,9 @@ cfg = self.config 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)]) - self._cleanup_sys_modules(cpath) - self.register_objects(path) + cube_modnames = cfg.appobjects_cube_modnames(cube) + self._cleanup_sys_modules(cube_modnames) + self.register_modnames(modnames) CW_EVENT_MANAGER.emit('after-registry-reload') def load_file(self, filepath, modname): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/dataimport/importer.py --- a/cubicweb/dataimport/importer.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/dataimport/importer.py Fri Feb 03 13:37:32 2017 +0100 @@ -265,7 +265,7 @@ :param extid2eid: optional {extid: eid} dictionary giving information on existing entities. It will be completed during import. You may want to use :func:`cwuri2eid` to build it. - :param existing_relation: optional {rtype: set((subj eid, obj eid))} mapping giving information + :param existing_relations: optional {rtype: set((subj eid, obj eid))} mapping giving information on existing relations of a given type. You may want to use :class:`RelationMapping` to build it. diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/dataimport/massive_store.py --- a/cubicweb/dataimport/massive_store.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/dataimport/massive_store.py Fri Feb 03 13:37:32 2017 +0100 @@ -72,7 +72,6 @@ self.uuid = text_type(uuid4()).replace('-', '') self.slave_mode = slave_mode - self.eids_seq_range = eids_seq_range if metagen is None: metagen = stores.MetadataGenerator(cnx) self.metagen = metagen @@ -81,7 +80,7 @@ self.sql = cnx.system_sql self.schema = cnx.vreg.schema self.default_values = get_default_values(self.schema) - self.get_next_eid = lambda g=self._get_eid_gen(): next(g) + self.get_next_eid = lambda g=self._get_eid_gen(eids_seq_range): next(g) self._source_dbhelper = cnx.repo.system_source.dbhelper self._dbh = PGHelper(cnx) @@ -89,13 +88,13 @@ self._data_relations = defaultdict(list) self._initialized = {} - def _get_eid_gen(self): + def _get_eid_gen(self, eids_seq_range): """ Function getting the next eid. This is done by preselecting a given number of eids from the 'entities_id_seq', and then storing them""" while True: - last_eid = self._cnx.repo.system_source.create_eid(self._cnx, self.eids_seq_range) - for eid in range(last_eid - self.eids_seq_range + 1, last_eid + 1): + last_eid = self._cnx.repo.system_source.create_eid(self._cnx, eids_seq_range) + for eid in range(last_eid - eids_seq_range + 1, last_eid + 1): yield eid # master/slaves specific API diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/dataimport/test/test_massive_store.py --- a/cubicweb/dataimport/test/test_massive_store.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/dataimport/test/test_massive_store.py Fri Feb 03 13:37:32 2017 +0100 @@ -16,8 +16,6 @@ # with this program. If not, see . """Massive store test case""" -import itertools - from cubicweb.devtools import testlib, PostgresApptestConfiguration from cubicweb.devtools import startpgcluster, stoppgcluster from cubicweb.dataimport import ucsvreader, stores diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/devtools/devctl.py --- a/cubicweb/devtools/devctl.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/devtools/devctl.py Fri Feb 03 13:37:32 2017 +0100 @@ -37,6 +37,7 @@ from six.moves import input from logilab.common import STD_BLACKLIST +from logilab.common.modutils import clean_sys_modules from logilab.common.fileutils import ensure_fs_mode from logilab.common.shellutils import find @@ -100,24 +101,6 @@ return None -def cleanup_sys_modules(config): - # cleanup sys.modules, required when we're updating multiple cubes - appobjects_path = config.appobjects_path() - for name, mod in list(sys.modules.items()): - if mod is None: - # duh ? logilab.common.os for instance - del sys.modules[name] - continue - if not hasattr(mod, '__file__'): - continue - if mod.__file__ is None: - # odd/rare but real - continue - for path in appobjects_path: - if mod.__file__.startswith(path): - del sys.modules[name] - break - def generate_schema_pot(w, cubedir=None): """generate a pot file with schema specific i18n messages @@ -136,7 +119,7 @@ else: config = DevConfiguration() cube = libconfig = None - cleanup_sys_modules(config) + clean_sys_modules(config.appobjects_modnames()) schema = config.load_schema(remove_unused_rtypes=False) vreg = CWRegistryStore(config) # set_schema triggers objects registrations @@ -161,7 +144,7 @@ # (cubicweb incl.) from cubicweb.cwvreg import CWRegistryStore libschema = libconfig.load_schema(remove_unused_rtypes=False) - cleanup_sys_modules(libconfig) + clean_sys_modules(libconfig.appobjects_modnames()) libvreg = CWRegistryStore(libconfig) libvreg.set_schema(libschema) # trigger objects registration libafss = libvreg['uicfg']['autoform_section'] diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/devtools/fake.py --- a/cubicweb/devtools/fake.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/devtools/fake.py Fri Feb 03 13:37:32 2017 +0100 @@ -34,7 +34,6 @@ class FakeConfig(dict, BaseApptestConfiguration): translations = {} uiprops = {} - https_uiprops = {} apphome = None debugmode = False def __init__(self, appid='data', apphome=None, cubes=()): @@ -46,7 +45,6 @@ self['base-url'] = BASE_URL self['rql-cache-size'] = 3000 self.datadir_url = BASE_URL + 'data/' - self.https_datadir_url = (BASE_URL + 'data/').replace('http://', 'https://') def cubes(self, expand=False): return self._cubes @@ -69,7 +67,6 @@ def __init__(self, *args, **kwargs): if not (args or 'vreg' in kwargs): kwargs['vreg'] = FakeCWRegistryStore(FakeConfig(), initlog=False) - kwargs['https'] = False self._http_method = kwargs.pop('method', 'GET') self._url = kwargs.pop('url', None) if self._url is None: diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/devtools/test/unittest_devctl.py --- a/cubicweb/devtools/test/unittest_devctl.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/devtools/test/unittest_devctl.py Fri Feb 03 13:37:32 2017 +0100 @@ -20,11 +20,11 @@ import os import os.path as osp import sys -import tempfile -import shutil from subprocess import Popen, PIPE, STDOUT, check_output from unittest import TestCase +from cubicweb.devtools.testlib import TemporaryDirectory + def newcube(directory, name): cmd = ['cubicweb-ctl', 'newcube', '--directory', directory, name] @@ -47,12 +47,11 @@ expected_project_content = ['setup.py', 'test', 'MANIFEST.in', 'cubicweb_foo', 'cubicweb-foo.spec', 'debian', 'README', - 'tox.ini'] + 'tox.ini', 'development.ini'] expected_package_content = ['i18n', 'hooks.py', 'views.py', 'migration', 'entities.py', 'schema.py', '__init__.py', 'data', '__pkginfo__.py'] - tmpdir = tempfile.mkdtemp(prefix="temp-cwctl-newcube") - try: + with TemporaryDirectory(prefix="temp-cwctl-newcube") as tmpdir: retcode, stdout = newcube(tmpdir, 'foo') self.assertEqual(retcode, 0, msg=to_unicode(stdout)) project_dir = osp.join(tmpdir, 'cubicweb-foo') @@ -61,27 +60,21 @@ package_content = os.listdir(package_dir) self.assertItemsEqual(project_content, expected_project_content) self.assertItemsEqual(package_content, expected_package_content) - finally: - shutil.rmtree(tmpdir, ignore_errors=True) def test_flake8(self): """Ensure newcube built from skeleton is flake8-compliant""" - tmpdir = tempfile.mkdtemp(prefix="temp-cwctl-newcube-flake8") - try: + with TemporaryDirectory(prefix="temp-cwctl-newcube-flake8") as tmpdir: newcube(tmpdir, 'foo') cmd = [sys.executable, '-m', 'flake8', osp.join(tmpdir, 'cubicweb-foo', 'cubicweb_foo')] proc = Popen(cmd, stdout=PIPE, stderr=STDOUT) retcode = proc.wait() - finally: - shutil.rmtree(tmpdir, ignore_errors=True) self.assertEqual(retcode, 0, msg=to_unicode(proc.stdout.read())) def test_newcube_sdist(self): """Ensure sdist can be built from a new cube""" - tmpdir = tempfile.mkdtemp(prefix="temp-cwctl-newcube-sdist") - try: + with TemporaryDirectory(prefix="temp-cwctl-newcube-sdist") as tmpdir: newcube(tmpdir, 'foo') projectdir = osp.join(tmpdir, 'cubicweb-foo') cmd = [sys.executable, 'setup.py', 'sdist'] @@ -91,13 +84,10 @@ self.assertEqual(retcode, 0, stdout) distfpath = osp.join(projectdir, 'dist', 'cubicweb-foo-0.1.0.tar.gz') self.assertTrue(osp.isfile(distfpath)) - finally: - shutil.rmtree(tmpdir, ignore_errors=True) def test_newcube_install(self): """Ensure a new cube can be installed""" - tmpdir = tempfile.mkdtemp(prefix="temp-cwctl-newcube-install") - try: + with TemporaryDirectory(prefix="temp-cwctl-newcube-install") as tmpdir: newcube(tmpdir, 'foo') projectdir = osp.join(tmpdir, 'cubicweb-foo') env = os.environ.copy() @@ -120,8 +110,6 @@ self.assertItemsEqual(pkgcontent, [b'schema.py', b'entities.py', b'hooks.py', b'__init__.py', b'__pkginfo__.py', b'views.py']) - finally: - shutil.rmtree(tmpdir, ignore_errors=True) if __name__ == '__main__': diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/devtools/testlib.py --- a/cubicweb/devtools/testlib.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/devtools/testlib.py Fri Feb 03 13:37:32 2017 +0100 @@ -679,7 +679,8 @@ def list_boxes_for(self, rset): """returns the list of boxes that can be applied on `rset`""" req = rset.req - for box in self.vreg['ctxcomponents'].possible_objects(req, rset=rset): + for box in self.vreg['ctxcomponents'].possible_objects(req, rset=rset, + view=None): yield box def list_startup_views(self): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/entities/__init__.py --- a/cubicweb/entities/__init__.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/entities/__init__.py Fri Feb 03 13:37:32 2017 +0100 @@ -98,57 +98,15 @@ # meta data api ########################################################### - def dc_title(self): - """return a suitable *unicode* title for this entity""" - for rschema, attrschema in self.e_schema.attribute_definitions(): - if rschema.meta: - continue - value = self.cw_attr_value(rschema.type) - if value is not None: - # make the value printable (dates, floats, bytes, etc.) - return self.printable_value(rschema.type, value, attrschema.type, - format='text/plain') - return u'%s #%s' % (self.dc_type(), self.eid) - - def dc_long_title(self): - """return a more detailled title for this entity""" - return self.dc_title() - - def dc_description(self, format='text/plain'): - """return a suitable description for this entity""" - if 'description' in self.e_schema.subjrels: - return self.printable_value('description', format=format) - return u'' - - def dc_authors(self): - """return a suitable description for the author(s) of the entity""" - try: - return ', '.join(u.name() for u in self.owned_by) - except Unauthorized: - return u'' - - def dc_creator(self): - """return a suitable description for the creator of the entity""" - if self.creator: - return self.creator.name() - return u'' - - def dc_date(self, date_format=None):# XXX default to ISO 8601 ? - """return latest modification date of this entity""" - return self._cw.format_date(self.modification_date, date_format=date_format) - - def dc_type(self, form=''): - """return the display name for the type of this entity (translated)""" - return self.e_schema.display_name(self._cw, form) - - def dc_language(self): - """return language used by this entity (translated)""" - # check if entities has internationalizable attributes - # XXX one is enough or check if all String attributes are internationalizable? - for rschema, attrschema in self.e_schema.attribute_definitions(): - if rschema.rdef(self.e_schema, attrschema).internationalizable: - return self._cw._(self._cw.user.property_value('ui.language')) - return self._cw._(self._cw.vreg.property_value('ui.language')) + def __getattr__(self, name): + prefix = 'dc_' + if name.startswith(prefix): + # Proxy to IDublinCore adapter for bw compat. + adapted = self.cw_adapt_to('IDublinCore') + method = name[len(prefix):] + if hasattr(adapted, method): + return getattr(adapted, method) + raise AttributeError(name) @property def creator(self): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/entities/adapters.py --- a/cubicweb/entities/adapters.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/entities/adapters.py Fri Feb 03 13:37:32 2017 +0100 @@ -25,10 +25,71 @@ from logilab.mtconverter import TransformError from logilab.common.decorators import cached -from cubicweb import ValidationError, view, ViolatedConstraint, UniqueTogetherError +from cubicweb import (Unauthorized, ValidationError, view, ViolatedConstraint, + UniqueTogetherError) from cubicweb.predicates import is_instance, relation_possible, match_exception +class IDublinCoreAdapter(view.EntityAdapter): + __regid__ = 'IDublinCore' + __select__ = is_instance('Any') + + def title(self): + """Return a suitable *unicode* title for entity""" + entity = self.entity + for rschema, attrschema in entity.e_schema.attribute_definitions(): + if rschema.meta: + continue + value = entity.cw_attr_value(rschema.type) + if value is not None: + # make the value printable (dates, floats, bytes, etc.) + return entity.printable_value( + rschema.type, value, attrschema.type, format='text/plain') + return u'%s #%s' % (self.type(), entity.eid) + + def long_title(self): + """Return a more detailled title for entity""" + return self.title() + + def description(self, format='text/plain'): + """Return a suitable description for entity""" + if 'description' in self.entity.e_schema.subjrels: + return self.entity.printable_value('description', format=format) + return u'' + + def authors(self): + """Return a suitable description for the author(s) of the entity""" + try: + return u', '.join(u.name() for u in self.entity.owned_by) + except Unauthorized: + return u'' + + def creator(self): + """Return a suitable description for the creator of the entity""" + if self.entity.creator: + return self.entity.creator.name() + return u'' + + def date(self, date_format=None): # XXX default to ISO 8601 ? + """Return latest modification date of entity""" + return self._cw.format_date(self.entity.modification_date, + date_format=date_format) + + def type(self, form=''): + """Return the display name for the type of entity (translated)""" + return self.entity.e_schema.display_name(self._cw, form) + + def language(self): + """Return language used by this entity (translated)""" + eschema = self.entity.e_schema + # check if entities has internationalizable attributes + # XXX one is enough or check if all String attributes are internationalizable? + for rschema, attrschema in eschema.attribute_definitions(): + if rschema.rdef(eschema, attrschema).internationalizable: + return self._cw._(self._cw.user.property_value('ui.language')) + return self._cw._(self._cw.vreg.property_value('ui.language')) + + class IEmailableAdapter(view.EntityAdapter): __regid__ = 'IEmailable' __select__ = relation_possible('primary_email') | relation_possible('use_email') diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/entity.py --- a/cubicweb/entity.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/entity.py Fri Feb 03 13:37:32 2017 +0100 @@ -985,11 +985,10 @@ if not safe: raise rset = self._cw.empty_rset() + if cacheable: + self.cw_set_relation_cache(rtype, role, rset) if entities: - if cacheable: - self.cw_set_relation_cache(rtype, role, rset) - return self.related(rtype, role, entities=entities) - return list(rset.entities()) + return tuple(rset.entities()) else: return rset @@ -1251,7 +1250,7 @@ def cw_set_relation_cache(self, rtype, role, rset): """set cached values for the given relation""" if rset: - related = list(rset.entities(0)) + related = tuple(rset.entities(0)) rschema = self._cw.vreg.schema.rschema(rtype) if role == 'subject': rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1] diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/etwist/http.py --- a/cubicweb/etwist/http.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/etwist/http.py Fri Feb 03 13:37:32 2017 +0100 @@ -8,6 +8,7 @@ + class HTTPResponse(object): """An object representing an HTTP Response to be sent to the client. """ @@ -29,9 +30,14 @@ # add content-length if not present if (self._headers_out.getHeader('content-length') is None and self._stream is not None): - self._twreq.setHeader('content-length', len(self._stream)) + self._twreq.setHeader('content-length', len(self._stream)) def _finalize(self): + # cw_failed is set on errors such as "connection aborted by client". In + # such cases, req.finish() was already called and calling it a twice + # would crash + if getattr(self._twreq, 'cw_failed', False): + return # we must set code before writing anything, else it's too late if self._code is not None: self._twreq.setResponseCode(self._code) diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/etwist/request.py --- a/cubicweb/etwist/request.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/etwist/request.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -17,8 +17,7 @@ # with CubicWeb. If not, see . """Twisted request handler for CubicWeb""" - - +from six import text_type from cubicweb.web.request import CubicWebRequestBase @@ -27,19 +26,19 @@ """ from twisted .req to cubicweb .form req.files are put into .form[] """ - def __init__(self, req, vreg, https): + def __init__(self, req, vreg): self._twreq = req super(CubicWebTwistedRequestAdapter, self).__init__( - vreg, https, req.args, headers=req.received_headers) + vreg, req.args, headers=req.received_headers) for key, name_stream_list in req.files.items(): for name, stream in name_stream_list: if name is not None: - name = unicode(name, self.encoding) + name = text_type(name, self.encoding) self.form.setdefault(key, []).append((name, stream)) # 3.16.4 backward compat if len(self.form[key]) == 1: self.form[key] = self.form[key][0] - self.content = self._twreq.content # stream + self.content = self._twreq.content # stream def http_method(self): """returns 'POST', 'GET', 'HEAD', etc.""" @@ -53,7 +52,7 @@ :param includeparams: boolean indicating if GET form parameters should be kept in the path """ - path = self._twreq.uri[1:] # remove the root '/' + path = self._twreq.uri[1:] # remove the root '/' if not includeparams: path = path.split('?', 1)[0] return path diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/etwist/server.py --- a/cubicweb/etwist/server.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/etwist/server.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -17,14 +17,11 @@ # with CubicWeb. If not, see . """twisted server for CubicWeb web instances""" - import sys -import select import traceback import threading from cgi import FieldStorage, parse_header - -from six.moves.urllib.parse import urlsplit, urlunsplit +from functools import partial from cubicweb.statsd_logger import statsd_timeit @@ -44,6 +41,7 @@ from cubicweb.etwist.request import CubicWebTwistedRequestAdapter from cubicweb.etwist.http import HTTPResponse + def start_task(interval, func): lc = task.LoopingCall(func) # wait until interval has expired to actually start the task, else we have @@ -59,7 +57,6 @@ # checks done before daemonization (eg versions consistency) self.appli = CubicWebPublisher(repo, config) self.base_url = config['base-url'] - self.https_url = config['https-url'] global MAX_POST_LENGTH MAX_POST_LENGTH = config['max-post-length'] @@ -92,13 +89,20 @@ """Indicate which resource to use to process down the URL's path""" return self + def on_request_finished_ko(self, request, reason): + # annotate the twisted request so that we're able later to check for + # failure without having to dig into request's internal attributes such + # as _disconnected + request.cw_failed = True + self.warning('request finished abnormally: %s', reason) + def render(self, request): """Render a page from the root resource""" + finish_deferred = request.notifyFinish() + finish_deferred.addErrback(partial(self.on_request_finished_ko, request)) # reload modified files in debug mode if self.config.debugmode: self.config.uiprops.reload_if_needed() - if self.https_url: - self.config.https_uiprops.reload_if_needed() self.appli.vreg.reload_if_needed() if self.config['profile']: # default profiler don't trace threads return self.render_request(request) @@ -123,18 +127,11 @@ def _render_request(self, request): origpath = request.path host = request.host - # dual http/https access handling: expect a rewrite rule to prepend - # 'https' to the path to detect https access - https = False - if origpath.split('/', 2)[1] == 'https': - origpath = origpath[6:] - request.uri = request.uri[6:] - https = True if self.url_rewriter is not None: # XXX should occur before authentication? path = self.url_rewriter.rewrite(host, origpath, request) request.uri.replace(origpath, path, 1) - req = CubicWebTwistedRequestAdapter(request, self.appli.vreg, https) + req = CubicWebTwistedRequestAdapter(request, self.appli.vreg) try: ### Try to generate the actual request content content = self.appli.handle_request(req) diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/etwist/service.py --- a/cubicweb/etwist/service.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/etwist/service.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -38,15 +38,17 @@ from cubicweb import set_log_methods from cubicweb.cwconfig import CubicWebConfiguration as cwcfg + def _check_env(env): env_vars = ('CW_INSTANCES_DIR', 'CW_INSTANCES_DATA_DIR', 'CW_RUNTIME_DIR') for var in env_vars: if var not in env: - raise Exception('The environment variables %s must be set.' % \ + raise Exception('The environment variables %s must be set.' % ', '.join(env_vars)) if not env.get('USERNAME'): env['USERNAME'] = 'cubicweb' + class CWService(object, win32serviceutil.ServiceFramework): _svc_name_ = None _svc_display_name_ = None @@ -80,8 +82,7 @@ config.debugmode = False logger.info('starting cubicweb instance %s ', self.instance) config.info('clear ui caches') - for cachedir in ('uicache', 'uicachehttps'): - rm(join(config.appdatahome, cachedir, '*')) + rm(join(config.appdatahome, 'uicache', '*')) root_resource = CubicWebRootResource(config, config.repository()) website = server.Site(root_resource) # serve it via standard HTTP on port set in the configuration diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/hooks/test/unittest_security.py --- a/cubicweb/hooks/test/unittest_security.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/hooks/test/unittest_security.py Fri Feb 03 13:37:32 2017 +0100 @@ -51,6 +51,7 @@ cnx.commit() self.assertEqual(email.sender[0].eid, self.add_eid) + if __name__ == '__main__': from logilab.common.testlib import unittest_main unittest_main() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/pyramid/bwcompat.py --- a/cubicweb/pyramid/bwcompat.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/pyramid/bwcompat.py Fri Feb 03 13:37:32 2017 +0100 @@ -53,10 +53,6 @@ CubicWebPublisher.core_handle do """ - # XXX The main handler of CW forbid anonymous https connections - # I guess we can drop this "feature" but in doubt I leave this comment - # so we don't forget about it. (cdevienne) - req = request.cw_request vreg = request.registry['cubicweb.registry'] @@ -170,10 +166,6 @@ self.cwhandler = registry['cubicweb.handler'] def __call__(self, request): - if request.path.startswith('/https/'): - request.environ['PATH_INFO'] = request.environ['PATH_INFO'][6:] - assert not request.path.startswith('/https/') - request.scheme = 'https' try: response = self.handler(request) except httpexceptions.HTTPNotFound: diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/pyramid/core.py --- a/cubicweb/pyramid/core.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/pyramid/core.py Fri Feb 03 13:37:32 2017 +0100 @@ -126,12 +126,11 @@ self.path = request.upath_info vreg = request.registry['cubicweb.registry'] - https = request.scheme == 'https' post = request.params.mixed() headers_in = request.headers - super(CubicWebPyramidRequest, self).__init__(vreg, https, post, + super(CubicWebPyramidRequest, self).__init__(vreg, post, headers=headers_in) self.content = request.body_file_seekable @@ -157,9 +156,6 @@ else: self.form[param] = val - def is_secure(self): - return self._request.scheme == 'https' - def relative_path(self, includeparams=True): path = self._request.path_info[1:] if includeparams and self._request.query_string: @@ -213,7 +209,7 @@ :param request: A pyramid request :param vid: A CubicWeb view id - :param **kwargs: Keyword arguments to select and instanciate the view + :param kwargs: Keyword arguments to select and instanciate the view :returns: The rendered view content """ vreg = request.registry['cubicweb.registry'] diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/pyramid/pyramidctl.py --- a/cubicweb/pyramid/pyramidctl.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/pyramid/pyramidctl.py Fri Feb 03 13:37:32 2017 +0100 @@ -15,7 +15,7 @@ import threading import subprocess -from cubicweb import BadCommandUsage, ExecutionError +from cubicweb import ExecutionError from cubicweb.cwconfig import CubicWebConfiguration as cwcfg from cubicweb.cwctl import CWCTL, InstanceCommand, init_cmdline_log_threshold from cubicweb.pyramid import wsgi_application_from_cwconfig @@ -99,15 +99,6 @@ def info(self, msg): print('INFO - %s' % msg) - def ordered_instances(self): - instances = super(PyramidStartHandler, self).ordered_instances() - if (self['debug-mode'] or self['debug'] or self['reload']) \ - and len(instances) > 1: - raise BadCommandUsage( - '--debug-mode, --debug and --reload can be used on a single ' - 'instance only') - return instances - def quote_first_command_arg(self, arg): """ There's a bug in Windows when running an executable that's diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/pyramid/test/__init__.py --- a/cubicweb/pyramid/test/__init__.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/pyramid/test/__init__.py Fri Feb 03 13:37:32 2017 +0100 @@ -11,10 +11,7 @@ @classmethod def init_config(cls, config): super(PyramidCWTest, cls).init_config(config) - config.global_set_option('https-url', 'https://localhost.local/') config.global_set_option('anonymous-user', 'anon') - config.https_uiprops = None - config.https_datadir_url = None def setUp(self): # Skip CubicWebTestTC setUp diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/pyramid/test/test_bw_request.py --- a/cubicweb/pyramid/test/test_bw_request.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/pyramid/test/test_bw_request.py Fri Feb 03 13:37:32 2017 +0100 @@ -33,24 +33,6 @@ self.assertEqual(b'some content', req.content.read()) - def test_http_scheme(self): - req = CubicWebPyramidRequest( - self.make_request('/', { - 'wsgi.url_scheme': 'http'})) - - self.assertFalse(req.https) - - def test_https_scheme(self): - req = CubicWebPyramidRequest( - self.make_request('/', { - 'wsgi.url_scheme': 'https'})) - - self.assertTrue(req.https) - - def test_https_prefix(self): - r = self.webapp.get('/https/') - self.assertIn('https://', r.text) - def test_big_content(self): content = b'x' * 100001 diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/pyramid/test/test_login.py --- a/cubicweb/pyramid/test/test_login.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/pyramid/test/test_login.py Fri Feb 03 13:37:32 2017 +0100 @@ -3,6 +3,7 @@ from cubicweb.pyramid.test import PyramidCWTest + class LoginTestLangUrlPrefix(PyramidCWTest): @classmethod @@ -19,7 +20,6 @@ self.assertEqual(res.status_int, 303) - class LoginTest(PyramidCWTest): def test_login_form(self): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/req.py --- a/cubicweb/req.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/req.py Fri Feb 03 13:37:32 2017 +0100 @@ -278,9 +278,6 @@ parameters. Values are automatically URL quoted, and the publishing method to use may be specified or will be guessed. - if ``__secure__`` argument is True, the request will try to build a - https url. - raises :exc:`ValueError` if None is found in arguments """ # use *args since we don't want first argument to be "anonymous" to @@ -295,8 +292,10 @@ # not try to process it and directly call req.build_url() base_url = kwargs.pop('base_url', None) if base_url is None: - secure = kwargs.pop('__secure__', None) - base_url = self.base_url(secure=secure) + if kwargs.pop('__secure__', None) is not None: + warn('[3.25] __secure__ argument is deprecated', + DeprecationWarning, stacklevel=2) + base_url = self.base_url() path = self.build_url_path(method, kwargs) if not kwargs: return u'%s%s' % (base_url, path) @@ -327,9 +326,9 @@ necessary encoding / decoding. Also it's designed to quote each part of a url path and so the '/' character will be encoded as well. """ - if PY2 and isinstance(value, unicode): + if PY2 and isinstance(value, text_type): quoted = urlquote(value.encode(self.encoding), safe=safe) - return unicode(quoted, self.encoding) + return text_type(quoted, self.encoding) return urlquote(str(value), safe=safe) def url_unquote(self, quoted): @@ -340,12 +339,12 @@ """ if PY3: return urlunquote(quoted) - if isinstance(quoted, unicode): + if isinstance(quoted, text_type): quoted = quoted.encode(self.encoding) try: - return unicode(urlunquote(quoted), self.encoding) + return text_type(urlunquote(quoted), self.encoding) except UnicodeDecodeError: # might occurs on manually typed URLs - return unicode(urlunquote(quoted), 'iso-8859-1') + return text_type(urlunquote(quoted), 'iso-8859-1') def url_parse_qsl(self, querystring): """return a list of (key, val) found in the url quoted query string""" @@ -353,13 +352,13 @@ for key, val in parse_qsl(querystring): yield key, val return - if isinstance(querystring, unicode): + if isinstance(querystring, text_type): querystring = querystring.encode(self.encoding) for key, val in parse_qsl(querystring): try: - yield unicode(key, self.encoding), unicode(val, self.encoding) + yield text_type(key, self.encoding), text_type(val, self.encoding) except UnicodeDecodeError: # might occurs on manually typed URLs - yield unicode(key, 'iso-8859-1'), unicode(val, 'iso-8859-1') + yield text_type(key, 'iso-8859-1'), text_type(val, 'iso-8859-1') def rebuild_url(self, url, **newparams): """return the given url with newparams inserted. If any new params @@ -367,7 +366,7 @@ newparams may only be mono-valued. """ - if PY2 and isinstance(url, unicode): + if PY2 and isinstance(url, text_type): url = url.encode(self.encoding) schema, netloc, path, query, fragment = urlsplit(url) query = parse_qs(query) @@ -439,7 +438,7 @@ as_string = formatters[attrtype] except KeyError: self.error('given bad attrtype %s', attrtype) - return unicode(value) + return text_type(value) return as_string(value, self, props, displaytime) def format_date(self, date, date_format=None, time=False): @@ -502,13 +501,12 @@ 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 - """ - url = self._base_url(secure=secure) + def base_url(self, **kwargs): + """Return the root url of the instance.""" + secure = kwargs.pop('secure', None) + if secure is not None: + warn('[3.25] secure argument is deprecated', DeprecationWarning, stacklevel=2) + if kwargs: + raise TypeError('base_url got unexpected keyword arguments %s' % ', '.join(kwargs)) + url = self.vreg.config['base-url'] return url if url is None else url.rstrip('/') + '/' diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/rqlrewrite.py --- a/cubicweb/rqlrewrite.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/rqlrewrite.py Fri Feb 03 13:37:32 2017 +0100 @@ -580,6 +580,7 @@ done.add(rel) rschema = get_rschema(rel.r_type) if rschema.final or rschema.inlined: + subselect_vrefs = [] rel.children[0].name = varname # XXX explain why subselect.add_restriction(rel.copy(subselect)) for vref in rel.children[1].iget_nodes(n.VariableRef): @@ -592,6 +593,7 @@ "least uninline %s" % rel.r_type) subselect.append_selected(vref.copy(subselect)) aliases.append(vref.name) + subselect_vrefs.append(vref) self.select.remove_node(rel) # when some inlined relation has to be copied in the # subquery and that relation is optional, we need to @@ -602,14 +604,15 @@ # also, if some attributes or inlined relation of the # object variable are accessed, we need to get all those # from the subquery as well - if vref.name not in done and rschema.inlined: - # we can use vref here define in above for loop - ostinfo = vref.variable.stinfo - for orel in iter_relations(ostinfo): - orschema = get_rschema(orel.r_type) - if orschema.final or orschema.inlined: - todo.append( (vref.name, ostinfo) ) - break + for vref in subselect_vrefs: + if vref.name not in done and rschema.inlined: + # we can use vref here define in above for loop + ostinfo = vref.variable.stinfo + for orel in iter_relations(ostinfo): + orschema = get_rschema(orel.r_type) + if orschema.final or orschema.inlined: + todo.append( (vref.name, ostinfo) ) + break if need_null_test: snippetrqlst = n.Or( n.make_relation(subselect.get_variable(selectvar), 'is', diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/rset.py --- a/cubicweb/rset.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/rset.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -20,7 +20,7 @@ from warnings import warn -from six import PY3 +from six import PY3, text_type from six.moves import range from logilab.common import nullobject @@ -73,7 +73,7 @@ # set by the cursor which returned this resultset self.req = None # actions cache - self._rsetactions = None + self._actions_cache = None def __str__(self): if not self.rows: @@ -94,25 +94,26 @@ if not self.description: return pattern % (self.rql, len(self.rows), - '\n'.join(str(r) for r in rows)) + '\n'.join(str(r) for r in rows)) return pattern % (self.rql, len(self.rows), - '\n'.join('%s (%s)' % (r, d) - for r, d in zip(rows, self.description))) + '\n'.join('%s (%s)' % (r, d) + for r, d in zip(rows, self.description))) def possible_actions(self, **kwargs): - if self._rsetactions is None: - self._rsetactions = {} - if kwargs: - key = tuple(sorted(kwargs.items())) - else: - key = None - try: - return self._rsetactions[key] - except KeyError: + """Return possible actions on this result set. Should always be called with the same + arguments so it may be computed only once. + """ + key = tuple(sorted(kwargs.items())) + if self._actions_cache is None: actions = self.req.vreg['actions'].poss_visible_objects( self.req, rset=self, **kwargs) - self._rsetactions[key] = actions + self._actions_cache = (key, actions) return actions + else: + assert key == self._actions_cache[0], \ + 'unexpected new arguments for possible actions (%s vs %s)' % ( + key, self._actions_cache[0]) + return self._actions_cache[1] def __len__(self): """returns the result set's size""" @@ -120,7 +121,7 @@ def __getitem__(self, i): """returns the ith element of the result set""" - return self.rows[i] #ResultSetRow(self.rows[i]) + return self.rows[i] def __iter__(self): """Returns an iterator over rows""" @@ -132,7 +133,7 @@ # at least rql could be fixed now that we have union and sub-queries # but I tend to think that since we have that, we should not need this # method anymore (syt) - rset = ResultSet(self.rows+rset.rows, self.rql, self.args, + rset = ResultSet(self.rows + rset.rows, self.rql, self.args, self.description + rset.description) rset.req = self.req return rset @@ -163,7 +164,7 @@ rset = self.copy(rows, descr) for row, desc in zip(self.rows, self.description): nrow, ndesc = transformcb(row, desc) - if ndesc: # transformcb returns None for ndesc to skip that row + if ndesc: # transformcb returns None for ndesc to skip that row rows.append(nrow) descr.append(ndesc) rset.rowcount = len(rows) @@ -192,7 +193,6 @@ rset.rowcount = len(rows) return rset - def sorted_rset(self, keyfunc, reverse=False, col=0): """sorts the result set according to a given keyfunc @@ -308,7 +308,7 @@ newselect = stmts.Select() newselect.limit = limit newselect.offset = offset - aliases = [nodes.VariableRef(newselect.get_variable(chr(65+i), i)) + aliases = [nodes.VariableRef(newselect.get_variable(chr(65 + i), i)) for i in range(len(rqlst.children[0].selection))] for vref in aliases: newselect.append_selected(nodes.VariableRef(vref.variable)) @@ -336,7 +336,7 @@ :rtype: `ResultSet` """ - stop = limit+offset + stop = limit + offset rows = self.rows[offset:stop] descr = self.description[offset:stop] if inplace: @@ -375,11 +375,11 @@ return rqlstr # sounds like we get encoded or unicode string due to a bug in as_string if not encoded: - if isinstance(rqlstr, unicode): + if isinstance(rqlstr, text_type): return rqlstr - return unicode(rqlstr, encoding) + return text_type(rqlstr, encoding) else: - if isinstance(rqlstr, unicode): + if isinstance(rqlstr, text_type): return rqlstr.encode(encoding) return rqlstr @@ -592,7 +592,7 @@ if row != last: if last is not None: result[-1][1] = i - 1 - result.append( [i, None, row] ) + result.append([i, None, row]) last = row if last is not None: result[-1][1] = i @@ -665,7 +665,7 @@ try: entity = self.get_entity(row, index) return entity, rel.r_type - except NotAnEntity as exc: + except NotAnEntity: return None, None return None, None @@ -683,12 +683,14 @@ return rhs.eval(self.args) return None + def _get_variable(term): # XXX rewritten const # use iget_nodes for (hack) case where we have things like MAX(V) for vref in term.iget_nodes(nodes.VariableRef): return vref.variable + def attr_desc_iterator(select, selectidx, rootidx): """return an iterator on a list of 2-uple (index, attr_relation) localizing attribute relations of the main variable in a result's row diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/rtags.py --- a/cubicweb/rtags.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/rtags.py Fri Feb 03 13:37:32 2017 +0100 @@ -38,16 +38,26 @@ import logging -from warnings import warn from six import string_types from logilab.common.logging_ext import set_log_methods from logilab.common.registry import RegistrableInstance, yes + def _ensure_str_key(key): return tuple(str(k) for k in key) + +def rtags_chain(rtag): + """Return the rtags chain, starting from the given one, and going back through each parent rtag + up to the root (i.e. which as no parent). + """ + while rtag is not None: + yield rtag + rtag = rtag._parent + + class RegistrableRtags(RegistrableInstance): __registry__ = 'uicfg' __select__ = yes() @@ -67,8 +77,13 @@ # function given as __init__ argument and kept for bw compat _init = _initfunc = None - def __init__(self): + def __init__(self, parent=None, __module__=None): + super(RelationTags, self).__init__(__module__) self._tagdefs = {} + self._parent = parent + if parent is not None: + assert parent.__class__ is self.__class__, \ + 'inconsistent class for parent rtag {0}'.format(parent) def __repr__(self): # find a way to have more infos but keep it readable @@ -99,12 +114,12 @@ if check: for (stype, rtype, otype, tagged), value in list(self._tagdefs.items()): for ertype in (stype, rtype, otype): - if ertype != '*' and not ertype in schema: + if ertype != '*' and ertype not in schema: self.warning('removing rtag %s: %s, %s undefined in schema', (stype, rtype, otype, tagged), value, ertype) self.del_rtag(stype, rtype, otype, tagged) break - if self._init is not None: + if self._parent is None and self._init is not None: self.apply(schema, self._init) def apply(self, schema, func): @@ -121,6 +136,19 @@ # rtag declaration api #################################################### + def derive(self, module, select): + """Return a derivated of this relation tag, associated to given module and selector. + + This derivated will hold a set of specific rules but delegate to its "parent" relation tags + for unfound keys. + + >>> class_afs = uicfg.autoform_section.derive(__name__, is_instance('Class')) + """ + copied = self.__class__(self, __module__=__name__) + copied.__module__ = module + copied.__select__ = select + return copied + def tag_attribute(self, key, *args, **kwargs): key = list(key) key.append('*') @@ -141,8 +169,8 @@ assert len(key) == 4, 'bad key: %s' % list(key) if self._allowed_values is not None: assert tag in self._allowed_values, \ - '%r is not an allowed tag (should be in %s)' % ( - tag, self._allowed_values) + '%r is not an allowed tag (should be in %s)' % ( + tag, self._allowed_values) self._tagdefs[_ensure_str_key(key)] = tag return tag @@ -156,18 +184,21 @@ else: self.tag_object_of((desttype, attr, etype), *args, **kwargs) - # rtag runtime api ######################################################## def del_rtag(self, *key): del self._tagdefs[key] def get(self, *key): + """Return value for the given key, by looking from the most specific key to the more + generic (using '*' wildcards). For each key, look into this rtag and its parent rtags. + """ for key in reversed(self._get_keys(*key)): - try: - return self._tagdefs[key] - except KeyError: - continue + for rtag in rtags_chain(self): + try: + return rtag._tagdefs[key] + except KeyError: + continue return None def etype_get(self, etype, rtype, role, ttype='*'): @@ -177,7 +208,7 @@ # these are overridden by set_log_methods below # only defining here to prevent pylint from complaining - info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None + info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None class RelationTagsSet(RelationTags): @@ -192,17 +223,23 @@ return rtags def get(self, stype, rtype, otype, tagged): + """Return value for the given key, which is an union of the values found from the most + specific key to the more generic (using '*' wildcards). For each key, look into this rtag + and its parent rtags. + """ rtags = self.tag_container_cls() for key in self._get_keys(stype, rtype, otype, tagged): - try: - rtags.update(self._tagdefs[key]) - except KeyError: - continue + for rtag in rtags_chain(self): + try: + rtags.update(rtag._tagdefs[key]) + break + except KeyError: + continue return rtags class RelationTagsDict(RelationTagsSet): - """This class associates a set of tags to each key.""" + """This class associates a dictionary to each key.""" tag_container_cls = dict def tag_relation(self, key, tag): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/schema.py --- a/cubicweb/schema.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/schema.py Fri Feb 03 13:37:32 2017 +0100 @@ -19,6 +19,7 @@ from __future__ import print_function +import pkgutil import re from os.path import join, basename from hashlib import md5 @@ -325,6 +326,7 @@ keyarg = None rqlst.recover() return rql, found, keyarg + rqlst.where = nodes.Exists(rqlst.where) return rqlst.as_string(), None, None def _check(self, _cw, **kwargs): @@ -347,7 +349,9 @@ if keyarg is None: kwargs.setdefault('u', _cw.user.eid) try: - rset = _cw.execute(rql, kwargs, build_descr=True) + # ensure security is disabled + with getattr(_cw, 'cnx', _cw).security_enabled(read=False): + rset = _cw.execute(rql, kwargs, build_descr=True) except NotImplementedError: self.critical('cant check rql expression, unsupported rql %s', rql) if self.eid is not None: @@ -1366,19 +1370,12 @@ """ schemacls = CubicWebSchema - def load(self, config, path=(), **kwargs): + def load(self, config, modnames=(['cubicweb', 'cubicweb.schemas.bootstrap'],), **kwargs): """return a Schema instance from the schema definition read from """ return super(BootstrapSchemaLoader, self).load( - path, config.appid, register_base_types=False, **kwargs) - - def _load_definition_files(self, cubes=None): - # bootstraping, ignore cubes - filepath = join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py') - self.info('loading %s', filepath) - with tempattr(ybo, 'PACKAGE', 'cubicweb'): # though we don't care here - self.handle_file(filepath) + modnames, name=config.appid, register_base_types=False, **kwargs) def unhandled_file(self, filepath): """called when a file without handler associated has been found""" @@ -1399,30 +1396,12 @@ from """ self.info('loading %s schemas', ', '.join(config.cubes())) - self.extrapath = config.extrapath - if config.apphome: - path = tuple(reversed([config.apphome] + config.cubes_path())) - else: - path = tuple(reversed(config.cubes_path())) try: - return super(CubicWebSchemaLoader, self).load(config, path=path, **kwargs) + return super(CubicWebSchemaLoader, self).load(config, config.schema_modnames(), **kwargs) finally: # we've to cleanup modules imported from cubicweb.schemas as well cleanup_sys_modules([join(cubicweb.CW_SOFTWARE_ROOT, 'schemas')]) - def _load_definition_files(self, cubes): - for filepath in (join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py'), - join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'base.py'), - join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'workflow.py'), - join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'Bookmark.py')): - self.info('loading %s', filepath) - with tempattr(ybo, 'PACKAGE', 'cubicweb'): - self.handle_file(filepath) - for cube in cubes: - for filepath in self.get_schema_files(cube): - with tempattr(ybo, 'PACKAGE', basename(cube)): - self.handle_file(filepath) - # these are overridden by set_log_methods below # only defining here to prevent pylint from complaining info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/server/repository.py --- a/cubicweb/server/repository.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/server/repository.py Fri Feb 03 13:37:32 2017 +0100 @@ -150,6 +150,47 @@ pass +class _CnxSetPool(object): + + def __init__(self, source, size): + self._cnxsets = [] + self._queue = queue.Queue() + for i in range(size): + cnxset = source.wrapped_connection() + self._cnxsets.append(cnxset) + self._queue.put_nowait(cnxset) + super(_CnxSetPool, self).__init__() + + def qsize(self): + return self._queue.qsize() + + def get(self): + try: + return self._queue.get(True, timeout=5) + except queue.Empty: + raise Exception('no connections set available after 5 secs, probably either a ' + 'bug in code (too many uncommited/rolled back ' + 'connections) or too much load on the server (in ' + 'which case you can try to set a bigger ' + 'connections pool size)') + + def release(self, cnxset): + self._queue.put_nowait(cnxset) + + def __iter__(self): + for cnxset in self._cnxsets: + yield cnxset + + def close(self): + q = self._queue + while not q.empty(): + cnxset = q.get_nowait() + try: + cnxset.close(True) + except Exception: + self.exception('error while closing %s' % cnxset) + + class Repository(object): """a repository provides access to a set of persistent storages for entities and relations @@ -208,10 +249,9 @@ # reload configuration from file and could reset a manually set pool # size. pool_size = config['connections-pool-size'] - self._cnxsets_pool = queue.Queue() - # 0. init a cnxset that will be used to fetch bootstrap information from + # 0. init a cnxset of size 1 that will be used to fetch bootstrap information from # the database - self._cnxsets_pool.put_nowait(self.system_source.wrapped_connection()) + self.cnxsets = _CnxSetPool(self.system_source, 1) # 1. set used cubes if config.creating or not config.read_instance_schema: config.bootstrap_cubes() @@ -259,12 +299,8 @@ self.vreg.init_properties(self.properties()) # 4. close initialization connection set and reopen fresh ones for # proper initialization - self._get_cnxset().close(True) - # list of available cnxsets (can't iterate on a Queue) - self.cnxsets = [] - for i in range(pool_size): - self.cnxsets.append(self.system_source.wrapped_connection()) - self._cnxsets_pool.put_nowait(self.cnxsets[-1]) + self.cnxsets.close() + self.cnxsets = _CnxSetPool(self.system_source, pool_size) # internals ############################################################### @@ -396,19 +432,6 @@ """start function in a separated thread""" utils.RepoThread(func, self._running_threads).start() - def _get_cnxset(self): - try: - return self._cnxsets_pool.get(True, timeout=5) - except queue.Empty: - raise Exception('no connections set available after 5 secs, probably either a ' - 'bug in code (too many uncommited/rolled back ' - 'connections) or too much load on the server (in ' - 'which case you can try to set a bigger ' - 'connections pool size)') - - def _free_cnxset(self, cnxset): - self._cnxsets_pool.put_nowait(cnxset) - def shutdown(self): """called on server stop event to properly close opened sessions and connections @@ -430,13 +453,7 @@ thread.join() self.info('thread %s finished', thread.getName()) self.close_sessions() - while not self._cnxsets_pool.empty(): - cnxset = self._cnxsets_pool.get_nowait() - try: - cnxset.close(True) - except Exception: - self.exception('error while closing %s' % cnxset) - continue + self.cnxsets.close() hits, misses = self.querier.cache_hit, self.querier.cache_miss try: self.info('rql st cache hit/miss: %s/%s (%s%% hits)', hits, misses, diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/server/session.py --- a/cubicweb/server/session.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/server/session.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -111,27 +111,19 @@ assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL) self.cnx = cnx self.mode = mode - self.categories = categories - self.oldmode = None - self.changes = () + self.categories = set(categories) + self.old_mode = None + self.old_categories = None def __enter__(self): - self.oldmode = self.cnx.hooks_mode - self.cnx.hooks_mode = self.mode - if self.mode is HOOKS_DENY_ALL: - self.changes = self.cnx.enable_hook_categories(*self.categories) - else: - self.changes = self.cnx.disable_hook_categories(*self.categories) + self.old_mode = self.cnx._hooks_mode + self.old_categories = self.cnx._hooks_categories + self.cnx._hooks_mode = self.mode + self.cnx._hooks_categories = self.categories def __exit__(self, exctype, exc, traceback): - try: - if self.categories: - if self.mode is HOOKS_DENY_ALL: - self.cnx.disable_hook_categories(*self.categories) - else: - self.cnx.enable_hook_categories(*self.categories) - finally: - self.cnx.hooks_mode = self.oldmode + self.cnx._hooks_mode = self.old_mode + self.cnx._hooks_categories = self.old_categories @deprecated('[3.17] use .security_enabled instead') @@ -240,13 +232,8 @@ Hooks controls: - :attr:`hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`. - - :attr:`enabled_hook_cats`, when :attr:`hooks_mode` is - `HOOKS_DENY_ALL`, this set contains hooks categories that are enabled. - - :attr:`disabled_hook_cats`, when :attr:`hooks_mode` is - `HOOKS_ALLOW_ALL`, this set contains hooks categories that are disabled. + .. automethod:: cubicweb.server.session.Connection.deny_all_hooks_but + .. automethod:: cubicweb.server.session.Connection.allow_all_hooks_but Security level Management: @@ -285,9 +272,13 @@ self.commit_state = None # hook control attribute - self.hooks_mode = HOOKS_ALLOW_ALL - self.disabled_hook_cats = set() - self.enabled_hook_cats = set() + # `_hooks_mode`, may be either `HOOKS_ALLOW_ALL` or `HOOKS_DENY_ALL`. + self._hooks_mode = HOOKS_ALLOW_ALL + # `_hooks_categories`, when :attr:`_hooks_mode` is `HOOKS_DENY_ALL`, + # this set contains hooks categories that are enabled ; + # when :attr:`_hooks_mode` is `HOOKS_ALLOW_ALL`, it contains hooks + # categories that are disabled. + self._hooks_categories = set() self.pruned_hooks_cache = {} # security control attributes @@ -385,7 +376,7 @@ def __enter__(self): assert not self._open self._open = True - self.cnxset = self.repo._get_cnxset() + self.cnxset = self.repo.cnxsets.get() if self.lang is None: self.set_language(self.user.prefered_language()) return self @@ -395,7 +386,7 @@ self.rollback() self._open = False self.cnxset.cnxset_freed() - self.repo._free_cnxset(self.cnxset) + self.repo.cnxsets.release(self.cnxset) self.cnxset = None @contextmanager @@ -506,14 +497,11 @@ return self.transaction_data.get('ecache', {}).values() @_open_only - def drop_entity_cache(self, eid=None): - """drop entity from the cache - - If eid is None, the whole cache is dropped""" - if eid is None: - self.transaction_data.pop('ecache', None) - else: - del self.transaction_data['ecache'][eid] + def drop_entity_cache(self): + """Drop the whole entity cache.""" + for entity in self.cached_entities(): + entity.cw_clear_all_caches() + self.transaction_data.pop('ecache', None) # relations handling ####################################################### @@ -679,60 +667,26 @@ @_open_only def allow_all_hooks_but(self, *categories): + """Context manager to enable all hooks but those in the given + categories. + """ return _hooks_control(self, HOOKS_ALLOW_ALL, *categories) @_open_only def deny_all_hooks_but(self, *categories): - return _hooks_control(self, HOOKS_DENY_ALL, *categories) - - @_open_only - def disable_hook_categories(self, *categories): - """disable the given hook categories: - - - on HOOKS_DENY_ALL mode, ensure those categories are not enabled - - on HOOKS_ALLOW_ALL mode, ensure those categories are disabled + """Context manager to disable all hooks but those in the given + categories. """ - changes = set() - self.pruned_hooks_cache.clear() - categories = set(categories) - if self.hooks_mode is HOOKS_DENY_ALL: - enabledcats = self.enabled_hook_cats - changes = enabledcats & categories - enabledcats -= changes # changes is small hence faster - else: - disabledcats = self.disabled_hook_cats - changes = categories - disabledcats - disabledcats |= changes # changes is small hence faster - return tuple(changes) - - @_open_only - def enable_hook_categories(self, *categories): - """enable the given hook categories: - - - on HOOKS_DENY_ALL mode, ensure those categories are enabled - - on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled - """ - changes = set() - self.pruned_hooks_cache.clear() - categories = set(categories) - if self.hooks_mode is HOOKS_DENY_ALL: - enabledcats = self.enabled_hook_cats - changes = categories - enabledcats - enabledcats |= changes # changes is small hence faster - else: - disabledcats = self.disabled_hook_cats - changes = disabledcats & categories - disabledcats -= changes # changes is small hence faster - return tuple(changes) + return _hooks_control(self, HOOKS_DENY_ALL, *categories) @_open_only def is_hook_category_activated(self, category): """return a boolean telling if the given category is currently activated or not """ - if self.hooks_mode is HOOKS_DENY_ALL: - return category in self.enabled_hook_cats - return category not in self.disabled_hook_cats + if self._hooks_mode is HOOKS_DENY_ALL: + return category in self._hooks_categories + return category not in self._hooks_categories @_open_only def is_hook_activated(self, hook): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/server/sources/native.py --- a/cubicweb/server/sources/native.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/server/sources/native.py Fri Feb 03 13:37:32 2017 +0100 @@ -380,7 +380,7 @@ # check full text index availibility if self.do_fti: if cnxset is None: - _cnxset = self.repo._get_cnxset() + _cnxset = self.repo.cnxsets.get() else: _cnxset = cnxset if not self.dbhelper.has_fti_table(_cnxset.cu): @@ -389,7 +389,7 @@ self.do_fti = False if cnxset is None: _cnxset.cnxset_freed() - self.repo._free_cnxset(_cnxset) + self.repo.cnxsets.release(_cnxset) def backup(self, backupfile, confirm, format='native'): """method called to create a backup of the source's data""" diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/server/test/unittest_checkintegrity.py --- a/cubicweb/server/test/unittest_checkintegrity.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/server/test/unittest_checkintegrity.py Fri Feb 03 13:37:32 2017 +0100 @@ -25,9 +25,9 @@ else: from io import StringIO -from cubicweb import devtools -from cubicweb.devtools.testlib import CubicWebTC -from cubicweb.server.checkintegrity import check, check_indexes, reindex_entities +from cubicweb import devtools # noqa: E402 +from cubicweb.devtools.testlib import CubicWebTC # noqa: E402 +from cubicweb.server.checkintegrity import check, check_indexes, reindex_entities # noqa: E402 class CheckIntegrityTC(unittest.TestCase): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/server/test/unittest_migractions.py --- a/cubicweb/server/test/unittest_migractions.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/server/test/unittest_migractions.py Fri Feb 03 13:37:32 2017 +0100 @@ -19,6 +19,7 @@ import os import os.path as osp +import sys from datetime import date from contextlib import contextmanager import tempfile @@ -77,13 +78,25 @@ # we have to read schema from the database to get eid for schema entities self.repo.set_schema(self.repo.deserialize_schema(), resetvreg=False) # hack to read the schema from data/migrschema - config = self.config - config.appid = osp.join(self.appid, 'migratedapp') - config._apphome = osp.join(HERE, config.appid) - global migrschema - migrschema = config.load_schema() - config.appid = self.appid - config._apphome = osp.join(HERE, self.appid) + + @contextmanager + def temp_app(config, appid, apphome): + old = config.apphome, config.appid + sys.path.remove(old[0]) + sys.path.insert(0, apphome) + config._apphome, config.appid = apphome, appid + try: + yield config + finally: + sys.path.remove(apphome) + sys.path.insert(0, old[0]) + config._apphome, config.appid = old + + appid = osp.join(self.appid, 'migratedapp') + apphome = osp.join(HERE, appid) + with temp_app(self.config, appid, apphome) as config: + global migrschema + migrschema = config.load_schema() def setUp(self): self.configcls.cls_adjust_sys_path() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/server/test/unittest_session.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/server/test/unittest_session.py Fri Feb 03 13:37:32 2017 +0100 @@ -0,0 +1,54 @@ +# copyright 2017 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 +from cubicweb.server import session + + +class HooksControlTC(CubicWebTC): + + def test_hooks_control(self): + with self.admin_access.repo_cnx() as cnx: + self.assertEqual(cnx._hooks_mode, session.HOOKS_ALLOW_ALL) + self.assertEqual(cnx._hooks_categories, set()) + + with cnx.deny_all_hooks_but('metadata'): + self.assertEqual(cnx._hooks_mode, session.HOOKS_DENY_ALL) + self.assertEqual(cnx._hooks_categories, set(['metadata'])) + + with cnx.deny_all_hooks_but(): + self.assertEqual(cnx._hooks_categories, set()) + self.assertEqual(cnx._hooks_categories, set(['metadata'])) + + with cnx.deny_all_hooks_but('integrity'): + self.assertEqual(cnx._hooks_categories, set(['integrity'])) + self.assertEqual(cnx._hooks_categories, set(['metadata'])) + + with cnx.allow_all_hooks_but('integrity'): + self.assertEqual(cnx._hooks_mode, session.HOOKS_ALLOW_ALL) + self.assertEqual(cnx._hooks_categories, set(['integrity'])) + self.assertEqual(cnx._hooks_mode, session.HOOKS_DENY_ALL) + self.assertEqual(cnx._hooks_categories, set(['metadata'])) + + self.assertEqual(cnx._hooks_mode, session.HOOKS_ALLOW_ALL) + self.assertEqual(cnx._hooks_categories, set()) + + +if __name__ == '__main__': + import unittest + unittest.main() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/server/utils.py --- a/cubicweb/server/utils.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/server/utils.py Fri Feb 03 13:37:32 2017 +0100 @@ -66,7 +66,7 @@ """return the encrypted password using the given salt or a generated one """ if salt is None: - return _CRYPTO_CTX.encrypt(passwd).encode('ascii') + return _CRYPTO_CTX.hash(passwd).encode('ascii') # empty hash, accept any password for backwards compat if salt == '': return salt diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/skeleton/MANIFEST.in.tmpl --- a/cubicweb/skeleton/MANIFEST.in.tmpl Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/skeleton/MANIFEST.in.tmpl Fri Feb 03 13:37:32 2017 +0100 @@ -5,6 +5,6 @@ recursive-include cubicweb_%(cubename)s/i18n *.po recursive-include cubicweb_%(cubename)s/wdoc * recursive-include test/data bootstrap_cubes *.py -include tox.ini +include *.ini recursive-include debian changelog compat control copyright rules include cubicweb-%(cubename)s.spec diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/skeleton/cubicweb_CUBENAME/__init__.py.tmpl --- a/cubicweb/skeleton/cubicweb_CUBENAME/__init__.py.tmpl Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/skeleton/cubicweb_CUBENAME/__init__.py.tmpl Fri Feb 03 13:37:32 2017 +0100 @@ -2,3 +2,12 @@ %(longdesc)s """ + + +def pyramid_main(global_config, **settings): + """Return a Pyramid WSGI application bound to a CubicWeb repository.""" + from pyramid.config import Configurator + config = Configurator(settings=settings) + config.include('cubicweb.pyramid') + config.scan() + return config.make_wsgi_app() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/skeleton/development.ini.tmpl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/skeleton/development.ini.tmpl Fri Feb 03 13:37:32 2017 +0100 @@ -0,0 +1,76 @@ +### +# app configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:%(distname)s + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +## +# CubicWeb instance settings +# http://cubicweb.readthedocs.io/en/latest/book/pyramid/settings/ +## +cubicweb.instance = %%(instance)s +cubicweb.bwcompat = True +cubicweb.debug = True +# cubicweb.auth.authtkt.persistent.secret = +cubicweb.auth.authtkt.persistent.secure = False +# cubicweb.auth.authtkt.session.secret = +cubicweb.auth.authtkt.session.secure = False + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = 127.0.0.1:6543 [::1]:6543 + +### +# logging configuration +# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, cubicweb_%(cubename)s, cubicweb + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_cubicweb] +level = INFO +handlers = console +qualname = cubicweb + +[logger_cubicweb_%(cubename)s] +level = DEBUG +handlers = console +qualname = cubicweb_%(cubename)s + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %%(asctime)s %%(levelname)-5.5s [%%(name)s:%%(lineno)s][%%(threadName)s] %%(message)s diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/skeleton/setup.py.tmpl --- a/cubicweb/skeleton/setup.py.tmpl Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/skeleton/setup.py.tmpl Fri Feb 03 13:37:32 2017 +0100 @@ -77,6 +77,9 @@ 'cubicweb.cubes': [ '%(cubename)s=cubicweb_%(cubename)s', ], + 'paste.app_factory': [ + 'main=cubicweb_%(cubename)s:pyramid_main', + ], }, zip_safe=False, ) diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/sobjects/notification.py --- a/cubicweb/sobjects/notification.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/sobjects/notification.py Fri Feb 03 13:37:32 2017 +0100 @@ -187,7 +187,7 @@ kwargs.update({'user': self.user_data['login'], 'eid': entity.eid, 'etype': entity.dc_type(), - 'url': entity.absolute_url(__secure__=True), + 'url': entity.absolute_url(), 'title': entity.dc_long_title(),}) return kwargs diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/sobjects/services.py --- a/cubicweb/sobjects/services.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/sobjects/services.py Fri Feb 03 13:37:32 2017 +0100 @@ -54,7 +54,7 @@ results['nb_active_threads'] = threading.activeCount() looping_tasks = repo._tasks_manager._looping_tasks results['looping_tasks'] = [(t.name, t.interval) for t in looping_tasks] - results['available_cnxsets'] = repo._cnxsets_pool.qsize() + results['available_cnxsets'] = repo.cnxsets.qsize() results['threads'] = [t.name for t in threading.enumerate()] return results diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/test/data/views.py --- a/cubicweb/test/data/views.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/test/data/views.py Fri Feb 03 13:37:32 2017 +0100 @@ -15,12 +15,13 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -from cubicweb.web.views import xmlrss -xmlrss.RSSIconBox.visible = True - from cubicweb.predicates import match_user_groups from cubicweb.server import Service +from cubicweb.web.views import xmlrss + + +xmlrss.RSSIconBox.visible = True class TestService(Service): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/test/data_schemareader/schema.py --- a/cubicweb/test/data_schemareader/schema.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/test/data_schemareader/schema.py Fri Feb 03 13:37:32 2017 +0100 @@ -11,6 +11,7 @@ 'CWSource', inlined=True, cardinality='1*', composite='object', __permissions__=RELATION_MANAGERS_PERMISSIONS) + cw_for_source = CWSourceSchemaConfig.get_relation('cw_for_source') cw_for_source.__permissions__ = {'read': ('managers', 'users'), 'add': ('managers',), diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/test/unittest_cubes.py --- a/cubicweb/test/unittest_cubes.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/test/unittest_cubes.py Fri Feb 03 13:37:32 2017 +0100 @@ -20,33 +20,31 @@ from contextlib import contextmanager import os from os import path -import shutil import sys -import tempfile import unittest from six import PY2 from cubicweb import _CubesImporter from cubicweb.cwconfig import CubicWebConfiguration +from cubicweb.devtools.testlib import TemporaryDirectory @contextmanager def temp_cube(): - tempdir = tempfile.mkdtemp() - try: - libdir = path.join(tempdir, 'libpython') - cubedir = path.join(libdir, 'cubicweb_foo') - os.makedirs(cubedir) - with open(path.join(cubedir, '__init__.py'), 'w') as f: - f.write('"""cubicweb_foo application package"""') - with open(path.join(cubedir, 'bar.py'), 'w') as f: - f.write('baz = 1') - sys.path.append(libdir) - yield cubedir - finally: - shutil.rmtree(tempdir) - sys.path.remove(libdir) + with TemporaryDirectory() as tempdir: + try: + libdir = path.join(tempdir, 'libpython') + cubedir = path.join(libdir, 'cubicweb_foo') + os.makedirs(cubedir) + with open(path.join(cubedir, '__init__.py'), 'w') as f: + f.write('"""cubicweb_foo application package"""') + with open(path.join(cubedir, 'bar.py'), 'w') as f: + f.write('baz = 1') + sys.path.append(libdir) + yield cubedir + finally: + sys.path.remove(libdir) class CubesImporterTC(unittest.TestCase): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/test/unittest_cwconfig.py --- a/cubicweb/test/unittest_cwconfig.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/test/unittest_cwconfig.py Fri Feb 03 13:37:32 2017 +0100 @@ -17,8 +17,12 @@ # with CubicWeb. If not, see . """cubicweb.cwconfig unit tests""" +import contextlib +import compileall +import functools import sys import os +import pkgutil from os.path import dirname, join, abspath from pkg_resources import EntryPoint, Distribution import unittest @@ -31,7 +35,8 @@ from cubicweb.devtools import ApptestConfiguration from cubicweb.devtools.testlib import BaseTestCase, TemporaryDirectory -from cubicweb.cwconfig import _find_prefix +from cubicweb.cwconfig import ( + CubicWebConfiguration, _find_prefix, _expand_modname) def unabsolutize(path): @@ -44,6 +49,50 @@ raise Exception('duh? %s' % path) +def templibdir(func): + """create a temporary directory and insert it in sys.path""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + with TemporaryDirectory() as libdir: + sys.path.insert(0, libdir) + try: + args = args + (libdir,) + return func(*args, **kwargs) + finally: + sys.path.remove(libdir) + return wrapper + + +def create_filepath(filepath): + filedir = dirname(filepath) + if not os.path.exists(filedir): + os.makedirs(filedir) + with open(filepath, 'a'): + pass + + +@contextlib.contextmanager +def temp_config(appid, instance_dir, cubes_dir, cubes): + """context manager that create a config object with specified appid, + instance_dir, cubes_dir and cubes""" + cls = CubicWebConfiguration + old = (cls._INSTANCES_DIR, cls.CUBES_DIR, cls.CUBES_PATH, + sys.path[:], sys.meta_path[:]) + old_modules = set(sys.modules) + try: + cls._INSTANCES_DIR, cls.CUBES_DIR, cls.CUBES_PATH = ( + instance_dir, cubes_dir, []) + config = cls(appid) + config._cubes = cubes + config.adjust_sys_path() + yield config + finally: + (cls._INSTANCES_DIR, cls.CUBES_DIR, cls.CUBES_PATH, + sys.path[:], sys.meta_path[:]) = old + for module in set(sys.modules) - old_modules: + del sys.modules[module] + + class CubicWebConfigurationTC(BaseTestCase): @classmethod @@ -129,17 +178,6 @@ self.assertEqual(self.config.expand_cubes(('email', 'comment')), ['email', 'comment', 'file']) - def test_appobjects_path(self): - path = [unabsolutize(p) for p in self.config.appobjects_path()] - self.assertEqual(path[0], 'entities') - self.assertCountEqual(path[1:4], ['web/views', 'sobjects', 'hooks']) - self.assertEqual(path[4], 'file/entities') - self.assertCountEqual(path[5:7], - ['file/views.py', 'file/hooks']) - self.assertEqual(path[7], 'email/entities.py') - self.assertCountEqual(path[8:10], - ['email/views', 'email/hooks.py']) - self.assertEqual(path[10:], ['test/data/entities.py', 'test/data/views.py']) def test_init_cubes_ignore_pyramid_cube(self): warning_msg = 'cubicweb-pyramid got integrated into CubicWeb' @@ -203,7 +241,8 @@ from cubes import mycube self.assertEqual(mycube.__path__, [join(self.custom_cubes_dir, 'mycube')]) # file cube should be overriden by the one found in data/cubes - if sys.modules.pop('cubes.file', None) and PY3: + sys.modules.pop('cubes.file') + if hasattr(cubes, 'file'): del cubes.file from cubes import file self.assertEqual(file.__path__, [join(self.custom_cubes_dir, 'file')]) @@ -331,5 +370,150 @@ os.environ['VIRTUAL_ENV'] = venv +class ModnamesTC(unittest.TestCase): + + @templibdir + def test_expand_modnames(self, libdir): + tempdir = join(libdir, 'lib') + filepaths = [ + join(tempdir, '__init__.py'), + join(tempdir, 'a.py'), + join(tempdir, 'b.py'), + join(tempdir, 'c.py'), + join(tempdir, 'b', '__init__.py'), + join(tempdir, 'b', 'a.py'), + join(tempdir, 'b', 'c.py'), + join(tempdir, 'b', 'd', '__init__.py'), + join(tempdir, 'e', 'e.py'), + ] + for filepath in filepaths: + create_filepath(filepath) + # not importable + self.assertEqual(list(_expand_modname('isnotimportable')), []) + # not a python package + self.assertEqual(list(_expand_modname('lib.e')), []) + self.assertEqual(list(_expand_modname('lib.a')), [ + ('lib.a', join(tempdir, 'a.py')), + ]) + # lib.b.d (subpackage) not to be imported + self.assertEqual(list(_expand_modname('lib.b')), [ + ('lib.b', join(tempdir, 'b', '__init__.py')), + ('lib.b.a', join(tempdir, 'b', 'a.py')), + ('lib.b.c', join(tempdir, 'b', 'c.py')), + ]) + self.assertEqual(list(_expand_modname('lib')), [ + ('lib', join(tempdir, '__init__.py')), + ('lib.a', join(tempdir, 'a.py')), + ('lib.c', join(tempdir, 'c.py')), + ]) + for source in ( + join(tempdir, 'c.py'), + join(tempdir, 'b', 'c.py'), + ): + if not PY3: + # ensure pyc file exists. + # Doesn't required for PY3 since it create __pycache__ + # directory and will not import if source file doesn't + # exists. + compileall.compile_file(source, force=True) + self.assertTrue(os.path.exists(source + 'c')) + # remove source file + os.remove(source) + self.assertEqual(list(_expand_modname('lib.c')), []) + self.assertEqual(list(_expand_modname('lib.b')), [ + ('lib.b', join(tempdir, 'b', '__init__.py')), + ('lib.b.a', join(tempdir, 'b', 'a.py')), + ]) + self.assertEqual(list(_expand_modname('lib')), [ + ('lib', join(tempdir, '__init__.py')), + ('lib.a', join(tempdir, 'a.py')), + ]) + + @templibdir + def test_schema_modnames(self, libdir): + for filepath in ( + join(libdir, 'schema.py'), + join(libdir, 'cubicweb_foo', '__init__.py'), + join(libdir, 'cubicweb_foo', 'schema', '__init__.py'), + join(libdir, 'cubicweb_foo', 'schema', 'a.py'), + join(libdir, 'cubicweb_foo', 'schema', 'b.py'), + join(libdir, 'cubes', '__init__.py'), + join(libdir, 'cubes', 'bar', '__init__.py'), + join(libdir, 'cubes', 'bar', 'schema.py'), + join(libdir, '_instance_dir', 'data1', 'schema.py'), + join(libdir, '_instance_dir', 'data2', 'noschema.py'), + ): + create_filepath(filepath) + expected = [ + ('cubicweb', 'cubicweb.schemas.bootstrap'), + ('cubicweb', 'cubicweb.schemas.base'), + ('cubicweb', 'cubicweb.schemas.workflow'), + ('cubicweb', 'cubicweb.schemas.Bookmark'), + ('bar', 'cubes.bar.schema'), + ('foo', 'cubes.foo.schema'), + ('foo', 'cubes.foo.schema.a'), + ('foo', 'cubes.foo.schema.b'), + ] + # app has schema file + instance_dir, cubes_dir = ( + join(libdir, '_instance_dir'), join(libdir, 'cubes')) + with temp_config('data1', instance_dir, cubes_dir, + ('foo', 'bar')) as config: + self.assertEqual(pkgutil.find_loader('schema').get_filename(), + join(libdir, '_instance_dir', + 'data1', 'schema.py')) + self.assertEqual(config.schema_modnames(), + expected + [('data', 'schema')]) + # app doesn't have schema file + with temp_config('data2', instance_dir, cubes_dir, + ('foo', 'bar')) as config: + self.assertEqual(pkgutil.find_loader('schema').get_filename(), + join(libdir, 'schema.py')) + self.assertEqual(config.schema_modnames(), expected) + + @templibdir + def test_appobjects_modnames(self, libdir): + for filepath in ( + join(libdir, 'entities.py'), + join(libdir, 'cubicweb_foo', '__init__.py'), + join(libdir, 'cubicweb_foo', 'entities', '__init__.py'), + join(libdir, 'cubicweb_foo', 'entities', 'a.py'), + join(libdir, 'cubicweb_foo', 'hooks.py'), + join(libdir, 'cubes', '__init__.py'), + join(libdir, 'cubes', 'bar', '__init__.py'), + join(libdir, 'cubes', 'bar', 'hooks.py'), + join(libdir, '_instance_dir', 'data1', 'entities.py'), + join(libdir, '_instance_dir', 'data2', 'hooks.py'), + ): + create_filepath(filepath) + instance_dir, cubes_dir = ( + join(libdir, '_instance_dir'), join(libdir, 'cubes')) + expected = [ + 'cubicweb.entities', + 'cubicweb.entities.adapters', + 'cubicweb.entities.authobjs', + 'cubicweb.entities.lib', + 'cubicweb.entities.schemaobjs', + 'cubicweb.entities.sources', + 'cubicweb.entities.wfobjs', + 'cubes.bar.hooks', + 'cubes.foo.entities', + 'cubes.foo.entities.a', + 'cubes.foo.hooks', + ] + # data1 has entities + with temp_config('data1', instance_dir, cubes_dir, + ('foo', 'bar')) as config: + config.cube_appobject_path = set(['entities', 'hooks']) + self.assertEqual(config.appobjects_modnames(), + expected + ['entities']) + # data2 has hooks + with temp_config('data2', instance_dir, cubes_dir, + ('foo', 'bar')) as config: + config.cube_appobject_path = set(['entities', 'hooks']) + self.assertEqual(config.appobjects_modnames(), + expected + ['hooks']) + + if __name__ == '__main__': unittest.main() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/test/unittest_entity.py --- a/cubicweb/test/unittest_entity.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/test/unittest_entity.py Fri Feb 03 13:37:32 2017 +0100 @@ -182,10 +182,15 @@ req.create_entity('Tag', name=tag) req.execute('SET X tags Y WHERE X is Tag, Y is Personne') self.assertEqual(len(p.related('tags', 'object', limit=2)), 2) + self.assertFalse(p.cw_relation_cached('tags', 'object')) self.assertEqual(len(p.related('tags', 'object')), 4) + self.assertTrue(p.cw_relation_cached('tags', 'object')) p.cw_clear_all_caches() + self.assertFalse(p.cw_relation_cached('tags', 'object')) self.assertEqual(len(p.related('tags', 'object', entities=True, limit=2)), 2) + self.assertFalse(p.cw_relation_cached('tags', 'object')) self.assertEqual(len(p.related('tags', 'object', entities=True)), 4) + self.assertTrue(p.cw_relation_cached('tags', 'object')) def test_related_targettypes(self): with self.admin_access.web_request() as req: diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/test/unittest_req.py --- a/cubicweb/test/unittest_req.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/test/unittest_req.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -19,25 +19,25 @@ from logilab.common.testlib import TestCase, unittest_main from cubicweb import ObjectNotFound from cubicweb.req import RequestSessionBase, FindEntityError -from cubicweb.devtools import ApptestConfiguration from cubicweb.devtools.testlib import CubicWebTC from cubicweb import Unauthorized + class RequestTC(TestCase): def test_rebuild_url(self): rebuild_url = RequestSessionBase(None).rebuild_url self.assertEqual(rebuild_url('http://logilab.fr?__message=pouet', __message='hop'), - 'http://logilab.fr?__message=hop') + 'http://logilab.fr?__message=hop') self.assertEqual(rebuild_url('http://logilab.fr', __message='hop'), - 'http://logilab.fr?__message=hop') + 'http://logilab.fr?__message=hop') self.assertEqual(rebuild_url('http://logilab.fr?vid=index', __message='hop'), - 'http://logilab.fr?__message=hop&vid=index') + 'http://logilab.fr?__message=hop&vid=index') def test_build_url(self): req = RequestSessionBase(None) - req.from_controller = lambda : 'view' + req.from_controller = lambda: 'view' req.relative_path = lambda includeparams=True: None - req.base_url = lambda secure=None: 'http://testing.fr/cubicweb/' + req.base_url = lambda: 'http://testing.fr/cubicweb/' self.assertEqual(req.build_url(), u'http://testing.fr/cubicweb/view') self.assertEqual(req.build_url(None), u'http://testing.fr/cubicweb/view') self.assertEqual(req.build_url('one'), u'http://testing.fr/cubicweb/one') @@ -49,8 +49,10 @@ req = RequestSessionBase(None) self.assertEqual(req.ensure_ro_rql('Any X WHERE X is CWUser'), None) self.assertEqual(req.ensure_ro_rql(' Any X WHERE X is CWUser '), None) - self.assertRaises(Unauthorized, req.ensure_ro_rql, 'SET X login "toto" WHERE X is CWUser') - self.assertRaises(Unauthorized, req.ensure_ro_rql, ' SET X login "toto" WHERE X is CWUser ') + self.assertRaises(Unauthorized, req.ensure_ro_rql, + 'SET X login "toto" WHERE X is CWUser') + self.assertRaises(Unauthorized, req.ensure_ro_rql, + ' SET X login "toto" WHERE X is CWUser ') class RequestCWTC(CubicWebTC): @@ -59,11 +61,15 @@ base_url = self.config['base-url'] with self.admin_access.repo_cnx() as session: self.assertEqual(session.base_url(), base_url) - assert 'https-url' not in self.config - self.assertEqual(session.base_url(secure=True), base_url) - secure_base_url = base_url.replace('http', 'https') - self.config.global_set_option('https-url', secure_base_url) - self.assertEqual(session.base_url(secure=True), secure_base_url) + + def test_secure_deprecated(self): + with self.admin_access.cnx() as cnx: + with self.assertWarns(DeprecationWarning): + cnx.base_url(secure=True) + with self.assertRaises(TypeError): + cnx.base_url(thing=42) + with self.assertWarns(DeprecationWarning): + cnx.build_url('ah', __secure__='whatever') def test_view_catch_ex(self): with self.admin_access.web_request() as req: diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/test/unittest_rqlrewrite.py --- a/cubicweb/test/unittest_rqlrewrite.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/test/unittest_rqlrewrite.py Fri Feb 03 13:37:32 2017 +0100 @@ -215,6 +215,12 @@ self.assertEqual(rqlst.as_string(), u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, A eid %(a)s WITH X,CD BEING (Any X,CD WHERE X creation_date CD, EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))') + def test_ambiguous_optional_same_exprs_constant(self): + rqlst = parse(u'Any A,AR,X WHERE A concerne X?, A ref AR, A eid %(a)s, X creation_date TODAY') + rewrite(rqlst, {('X', 'X'): ('X created_by U',),}, {'a': 3}) + self.assertEqual(rqlst.as_string(), + u'Any A,AR,X WHERE A concerne X?, A ref AR, A eid %(a)s WITH X BEING (Any X WHERE X creation_date TODAY, EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))') + def test_optional_var_inlined(self): c1 = ('X require_permission P') c2 = ('X inlined_card O, O require_permission P') diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/test/unittest_rset.py --- a/cubicweb/test/unittest_rset.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/test/unittest_rset.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,5 +1,5 @@ # coding: utf-8 -# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -44,12 +44,12 @@ def test_relations_description(self): """tests relations_description() function""" queries = { - 'Any U,L,M where U is CWUser, U login L, U mail M' : [(1, 'login', 'subject'), (2, 'mail', 'subject')], - 'Any U,L,M where U is CWUser, L is Foo, U mail M' : [(2, 'mail', 'subject')], - 'Any C,P where C is Company, C employs P' : [(1, 'employs', 'subject')], - 'Any C,P where C is Company, P employed_by P' : [], - 'Any C where C is Company, C employs P' : [], - } + 'Any U,L,M where U is CWUser, U login L, U mail M': [(1, 'login', 'subject'), (2, 'mail', 'subject')], + 'Any U,L,M where U is CWUser, L is Foo, U mail M': [(2, 'mail', 'subject')], + 'Any C,P where C is Company, C employs P': [(1, 'employs', 'subject')], + 'Any C,P where C is Company, P employed_by P': [], + 'Any C where C is Company, C employs P': [], + } for rql, relations in queries.items(): result = list(attr_desc_iterator(parse(rql).children[0], 0, 0)) self.assertEqual((rql, result), (rql, relations)) @@ -57,9 +57,10 @@ def test_relations_description_indexed(self): """tests relations_description() function""" queries = { - 'Any C,U,P,L,M where C is Company, C employs P, U is CWUser, U login L, U mail M' : - {0: [(2,'employs', 'subject')], 1: [(3,'login', 'subject'), (4,'mail', 'subject')]}, - } + 'Any C,U,P,L,M where C is Company, C employs P, U is CWUser, U login L, U mail M': + {0: [(2, 'employs', 'subject')], + 1: [(3, 'login', 'subject'), (4, 'mail', 'subject')]}, + } for rql, results in queries.items(): for idx, relations in results.items(): result = list(attr_desc_iterator(parse(rql).children[0], idx, idx)) @@ -118,17 +119,6 @@ # '%stask/title/go' % baseurl) # empty _restpath should not crash self.compare_urls(req.build_url('view', _restpath=''), baseurl) - self.assertNotIn('https', req.build_url('view', vid='foo', rql='yo', - __secure__=True)) - try: - self.config.global_set_option('https-url', 'https://testing.fr/') - self.assertTrue('https', req.build_url('view', vid='foo', rql='yo', - __secure__=True)) - self.compare_urls(req.build_url('view', vid='foo', rql='yo', - __secure__=True), - '%sview?vid=foo&rql=yo' % req.base_url(secure=True)) - finally: - self.config.global_set_option('https-url', None) def test_build(self): @@ -155,8 +145,6 @@ def test_limit_2(self): with self.admin_access.web_request() as req: - # drop user from cache for the sake of this test - req.drop_entity_cache(req.user.eid) rs = req.execute('Any E,U WHERE E is CWEType, E created_by U') # get entity on row 9. This will fill its created_by relation cache, # with cwuser on row 9 as well @@ -284,7 +272,7 @@ def test_get_entity_simple(self): with self.admin_access.web_request() as req: req.create_entity('CWUser', login=u'adim', upassword='adim', - surname=u'di mascio', firstname=u'adrien') + surname=u'di mascio', firstname=u'adrien') req.drop_entity_cache() e = req.execute('Any X,T WHERE X login "adim", X surname T').get_entity(0, 0) self.assertEqual(e.cw_attr_cache['surname'], 'di mascio') @@ -575,6 +563,13 @@ '(Any X,N WHERE X is CWGroup, X name N)' ')') + def test_possible_actions_cache(self): + with self.admin_access.web_request() as req: + rset = req.execute('Any D, COUNT(U) GROUPBY D WHERE U is CWUser, U creation_date D') + rset.possible_actions(argument='Value') + self.assertRaises(AssertionError, rset.possible_actions, argument='OtherValue') + self.assertRaises(AssertionError, rset.possible_actions, other_argument='Value') + def test_count_users_by_date(self): with self.admin_access.web_request() as req: rset = req.execute('Any D, COUNT(U) GROUPBY D WHERE U is CWUser, U creation_date D') diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/test/unittest_rtags.py --- a/cubicweb/test/unittest_rtags.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/test/unittest_rtags.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -15,81 +15,140 @@ # # 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.devtools.testlib import BaseTestCase from cubicweb.rtags import RelationTags, RelationTagsSet, RelationTagsDict -class RelationTagsTC(TestCase): + +class RelationTagsTC(BaseTestCase): + + def setUp(self): + self.rtags = RelationTags(__module__=__name__) + self.rtags.tag_subject_of(('Societe', 'travaille', '*'), 'primary') + self.rtags.tag_subject_of(('*', 'evaluee', '*'), 'secondary') + self.rtags.tag_object_of(('*', 'tags', '*'), 'generated') - def test_rtags_expansion(self): - rtags = RelationTags() - rtags.tag_subject_of(('Societe', 'travaille', '*'), 'primary') - rtags.tag_subject_of(('*', 'evaluee', '*'), 'secondary') - rtags.tag_object_of(('*', 'tags', '*'), 'generated') - self.assertEqual(rtags.get('Note', 'evaluee', '*', 'subject'), - 'secondary') - self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'), - 'primary') - self.assertEqual(rtags.get('Note', 'travaille', '*', 'subject'), - None) - self.assertEqual(rtags.get('Note', 'tags', '*', 'subject'), - None) - self.assertEqual(rtags.get('*', 'tags', 'Note', 'object'), - 'generated') - self.assertEqual(rtags.get('Tag', 'tags', '*', 'object'), - 'generated') + def test_expansion(self): + self.assertEqual(self.rtags.get('Note', 'evaluee', '*', 'subject'), + 'secondary') + self.assertEqual(self.rtags.get('Societe', 'travaille', '*', 'subject'), + 'primary') + self.assertEqual(self.rtags.get('Note', 'travaille', '*', 'subject'), + None) + self.assertEqual(self.rtags.get('Note', 'tags', '*', 'subject'), + None) + self.assertEqual(self.rtags.get('*', 'tags', 'Note', 'object'), + 'generated') + self.assertEqual(self.rtags.get('Tag', 'tags', '*', 'object'), + 'generated') -# self.assertEqual(rtags.rtag('evaluee', 'Note', 'subject'), set(('secondary', 'link'))) -# self.assertEqual(rtags.is_inlined('evaluee', 'Note', 'subject'), False) -# self.assertEqual(rtags.rtag('evaluee', 'Personne', 'subject'), set(('secondary', 'link'))) -# self.assertEqual(rtags.is_inlined('evaluee', 'Personne', 'subject'), False) -# self.assertEqual(rtags.rtag('ecrit_par', 'Note', 'object'), set(('inlineview', 'link'))) -# self.assertEqual(rtags.is_inlined('ecrit_par', 'Note', 'object'), True) -# class Personne2(Personne): -# id = 'Personne' -# __rtags__ = { -# ('evaluee', 'Note', 'subject') : set(('inlineview',)), -# } -# self.vreg.register(Personne2) -# rtags = Personne2.rtags -# self.assertEqual(rtags.rtag('evaluee', 'Note', 'subject'), set(('inlineview', 'link'))) -# self.assertEqual(rtags.is_inlined('evaluee', 'Note', 'subject'), True) -# self.assertEqual(rtags.rtag('evaluee', 'Personne', 'subject'), set(('secondary', 'link'))) -# self.assertEqual(rtags.is_inlined('evaluee', 'Personne', 'subject'), False) + def test_expansion_with_parent(self): + derived_rtags = self.rtags.derive(__name__, None) + derived_rtags.tag_subject_of(('Societe', 'travaille', '*'), 'secondary') + derived_rtags.tag_subject_of(('Note', 'evaluee', '*'), 'primary') + self.rtags.tag_object_of(('*', 'tags', '*'), 'hidden') + + self.assertEqual(derived_rtags.get('Note', 'evaluee', '*', 'subject'), + 'primary') + self.assertEqual(derived_rtags.get('Societe', 'evaluee', '*', 'subject'), + 'secondary') + self.assertEqual(derived_rtags.get('Societe', 'travaille', '*', 'subject'), + 'secondary') + self.assertEqual(derived_rtags.get('Note', 'travaille', '*', 'subject'), + None) + self.assertEqual(derived_rtags.get('*', 'tags', 'Note', 'object'), + 'hidden') - def test_rtagset_expansion(self): - rtags = RelationTagsSet() - rtags.tag_subject_of(('Societe', 'travaille', '*'), 'primary') - rtags.tag_subject_of(('*', 'travaille', '*'), 'secondary') - self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'), - set(('primary', 'secondary'))) - self.assertEqual(rtags.get('Note', 'travaille', '*', 'subject'), - set(('secondary',))) - self.assertEqual(rtags.get('Note', 'tags', "*", 'subject'), - set()) +class RelationTagsSetTC(BaseTestCase): + + def setUp(self): + self.rtags = RelationTagsSet(__module__=__name__) + self.rtags.tag_subject_of(('Societe', 'travaille', '*'), 'primary') + self.rtags.tag_subject_of(('*', 'travaille', '*'), 'secondary') + + def test_expansion(self): + self.assertEqual(self.rtags.get('Societe', 'travaille', '*', 'subject'), + set(('primary', 'secondary'))) + self.assertEqual(self.rtags.get('Note', 'travaille', '*', 'subject'), + set(('secondary',))) + self.assertEqual(self.rtags.get('Note', 'tags', "*", 'subject'), + set()) + + def test_expansion_with_parent(self): + derived_rtags = self.rtags.derive(__name__, None) + derived_rtags.tag_subject_of(('Societe', 'travaille', '*'), 'derived_primary') + self.assertEqual(derived_rtags.get('Societe', 'travaille', '*', 'subject'), + set(('derived_primary', 'secondary'))) + self.assertEqual(derived_rtags.get('Note', 'travaille', '*', 'subject'), + set(('secondary',))) + + derived_rtags.tag_subject_of(('*', 'travaille', '*'), 'derived_secondary') + self.assertEqual(derived_rtags.get('Societe', 'travaille', '*', 'subject'), + set(('derived_primary', 'derived_secondary'))) + self.assertEqual(derived_rtags.get('Note', 'travaille', '*', 'subject'), + set(('derived_secondary',))) + + self.assertEqual(derived_rtags.get('Note', 'tags', "*", 'subject'), + set()) + + +class RelationTagsDictTC(BaseTestCase): + + def setUp(self): + self.rtags = RelationTagsDict(__module__=__name__) + self.rtags.tag_subject_of(('Societe', 'travaille', '*'), + {'key1': 'val1', 'key2': 'val1'}) + self.rtags.tag_subject_of(('*', 'travaille', '*'), + {'key1': 'val0', 'key3': 'val0'}) + self.rtags.tag_subject_of(('Societe', 'travaille', '*'), + {'key2': 'val2'}) - def test_rtagdict_expansion(self): - rtags = RelationTagsDict() - rtags.tag_subject_of(('Societe', 'travaille', '*'), - {'key1': 'val1', 'key2': 'val1'}) - rtags.tag_subject_of(('*', 'travaille', '*'), - {'key1': 'val0', 'key3': 'val0'}) - rtags.tag_subject_of(('Societe', 'travaille', '*'), - {'key2': 'val2'}) - self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'), - {'key1': 'val1', 'key2': 'val2', 'key3': 'val0'}) - self.assertEqual(rtags.get('Note', 'travaille', '*', 'subject'), - {'key1': 'val0', 'key3': 'val0'}) - self.assertEqual(rtags.get('Note', 'tags', "*", 'subject'), - {}) + def test_expansion(self): + self.assertEqual(self.rtags.get('Societe', 'travaille', '*', 'subject'), + {'key1': 'val1', 'key2': 'val2', 'key3': 'val0'}) + self.assertEqual(self.rtags.get('Note', 'travaille', '*', 'subject'), + {'key1': 'val0', 'key3': 'val0'}) + self.assertEqual(self.rtags.get('Note', 'tags', "*", 'subject'), + {}) + + self.rtags.setdefault(('Societe', 'travaille', '*', 'subject'), 'key1', 'val4') + self.rtags.setdefault(('Societe', 'travaille', '*', 'subject'), 'key4', 'val4') + self.assertEqual(self.rtags.get('Societe', 'travaille', '*', 'subject'), + {'key1': 'val1', 'key2': 'val2', 'key3': 'val0', 'key4': 'val4'}) + + def test_expansion_with_parent(self): + derived_rtags = self.rtags.derive(__name__, None) - rtags.setdefault(('Societe', 'travaille', '*', 'subject'), 'key1', 'val4') - rtags.setdefault(('Societe', 'travaille', '*', 'subject'), 'key4', 'val4') - self.assertEqual(rtags.get('Societe', 'travaille', '*', 'subject'), - {'key1': 'val1', 'key2': 'val2', 'key3': 'val0', 'key4': 'val4'}) + derived_rtags.tag_subject_of(('Societe', 'travaille', '*'), + {'key0': 'val0'}) + self.assertEqual(derived_rtags.get('Societe', 'travaille', '*', 'subject'), + {'key0': 'val0', 'key1': 'val0', 'key3': 'val0'}) + self.assertEqual(derived_rtags.get('Note', 'travaille', '*', 'subject'), + {'key1': 'val0', 'key3': 'val0'}) + self.assertEqual(derived_rtags.get('Note', 'tags', "*", 'subject'), + {}) + + derived_rtags.tag_subject_of(('*', 'travaille', '*'), + {'key0': 'val00', 'key4': 'val4'}) + self.assertEqual(derived_rtags.get('Societe', 'travaille', '*', 'subject'), + {'key0': 'val0', 'key4': 'val4'}) + self.assertEqual(derived_rtags.get('Note', 'travaille', '*', 'subject'), + {'key0': 'val00', 'key4': 'val4'}) + + +class DeprecatedInstanceWithoutModule(BaseTestCase): + + def test_deprecated_instance_without_module(self): + class SubRelationTags(RelationTags): + pass + with self.assertWarnsRegex( + DeprecationWarning, + 'instantiate SubRelationTags with __module__=__name__', + ): + SubRelationTags() + if __name__ == '__main__': - unittest_main() + import unittest + unittest.main() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/test/unittest_schema.py --- a/cubicweb/test/unittest_schema.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/test/unittest_schema.py Fri Feb 03 13:37:32 2017 +0100 @@ -17,7 +17,7 @@ # with CubicWeb. If not, see . """unit tests for module cubicweb.schema""" -from os.path import join, dirname +from os.path import join, dirname, splitext from logilab.common.testlib import TestCase, unittest_main @@ -402,7 +402,8 @@ self.loader.post_build_callbacks = [] def _test(self, schemafile, msg): - self.loader.handle_file(join(DATADIR, schemafile)) + self.loader.handle_file(join(DATADIR, schemafile), + splitext(schemafile)[0]) sch = self.loader.schemacls('toto') with self.assertRaises(BadSchemaDefinition) as cm: fill_schema(sch, self.loader.defined, False) @@ -575,5 +576,32 @@ for r, role in schema[etype].composite_rdef_roles])) +class CWShemaTC(CubicWebTC): + + def test_transform_has_permission_match(self): + with self.admin_access.repo_cnx() as cnx: + eschema = cnx.vreg.schema['EmailAddress'] + rql_exprs = eschema.get_rqlexprs('update') + self.assertEqual(len(rql_exprs), 1) + self.assertEqual(rql_exprs[0].expression, + 'P use_email X, U has_update_permission P') + rql, found, keyarg = rql_exprs[0].transform_has_permission() + self.assertEqual(rql, 'Any X,P WHERE P use_email X, X eid %(x)s') + self.assertEqual(found, [(u'update', 1)]) + self.assertEqual(keyarg, None) + + def test_transform_has_permission_no_match(self): + with self.admin_access.repo_cnx() as cnx: + eschema = cnx.vreg.schema['EmailAddress'] + rql_exprs = eschema.get_rqlexprs('read') + self.assertEqual(len(rql_exprs), 1) + self.assertEqual(rql_exprs[0].expression, + 'U use_email X') + rql, found, keyarg = rql_exprs[0].transform_has_permission() + self.assertEqual(rql, 'Any X WHERE EXISTS(U use_email X, X eid %(x)s, U eid %(u)s)') + self.assertEqual(found, None) + self.assertEqual(keyarg, None) + + if __name__ == '__main__': unittest_main() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/application.py --- a/cubicweb/web/application.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/application.py Fri Feb 03 13:37:32 2017 +0100 @@ -125,8 +125,6 @@ """return a string giving the name of the cookie used to store the session identifier. """ - if req.https: - return '__%s_https_session' % self.vreg.config.appid return '__%s_session' % self.vreg.config.appid def get_session(self, req): @@ -158,7 +156,7 @@ def open_session(self, req): session = self.session_manager.open_session(req) sessioncookie = self.session_cookie(req) - secure = req.https and req.base_url().startswith('https://') + secure = req.base_url().startswith('https://') req.set_cookie(sessioncookie, session.sessionid, maxage=None, secure=secure, httponly=True) if not session.anonymous_session: @@ -252,15 +250,18 @@ return set_cnx req.set_cnx = wrap_set_cnx(req.set_cnx) + tstart, cstart = time(), clock() try: return self.main_handle_request(req) finally: cnx = req.cnx - if cnx: + if cnx and cnx.executed_queries: with self._logfile_lock: + tend, cend = time(), clock() try: result = ['\n' + '*' * 80] - result.append(req.url()) + result.append('%s -- (%.3f sec, %.3f CPU sec)' % ( + req.url(), tend - tstart, cend - cstart)) result += ['%s %s -- (%.3f sec, %.3f CPU sec)' % q for q in cnx.executed_queries] cnx.executed_queries = [] @@ -331,27 +332,20 @@ content = self.redirect_handler(req, ex) # Wrong, absent or Reseted credential except AuthenticationError: - # If there is an https url configured and - # the request does not use https, redirect to login form - https_url = self.vreg.config['https-url'] - if https_url and req.base_url() != https_url: - req.status_out = http_client.SEE_OTHER - req.headers_out.setHeader('location', https_url + 'login') + # We assume here that in http auth mode the user *May* provide + # Authentification Credential if asked kindly. + if self.vreg.config['auth-mode'] == 'http': + req.status_out = http_client.UNAUTHORIZED + # In the other case (coky auth) we assume that there is no way + # for the user to provide them... + # XXX But WHY ? else: - # We assume here that in http auth mode the user *May* provide - # Authentification Credential if asked kindly. - if self.vreg.config['auth-mode'] == 'http': - req.status_out = http_client.UNAUTHORIZED - # In the other case (coky auth) we assume that there is no way - # for the user to provide them... - # XXX But WHY ? - else: - req.status_out = http_client.FORBIDDEN - # If previous error handling already generated a custom content - # do not overwrite it. This is used by LogOut Except - # XXX ensure we don't actually serve content - if not content: - content = self.need_login_content(req) + req.status_out = http_client.FORBIDDEN + # If previous error handling already generated a custom content + # do not overwrite it. This is used by LogOut Except + # XXX ensure we don't actually serve content + if not content: + content = self.need_login_content(req) assert isinstance(content, binary_type) return content diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/data/cubicweb.edition.js --- a/cubicweb/web/data/cubicweb.edition.js Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/data/cubicweb.edition.js Fri Feb 03 13:37:32 2017 +0100 @@ -9,7 +9,7 @@ //============= Eproperty form functions =====================================// /** - * .. function:: setPropValueWidget(varname, tabindex) + * .. function:: setPropValueWidget(varname) * * called on CWProperty key selection: * - get the selected value @@ -17,16 +17,15 @@ * - fill associated div with the returned html * * * `varname`, the name of the variable as used in the original creation form - * * `tabindex`, the tabindex that should be set on the widget */ -function setPropValueWidget(varname, tabindex) { +function setPropValueWidget(varname) { var key = firstSelected(document.getElementById('pkey-subject:' + varname)); if (key) { var args = { fname: 'prop_widget', pageid: pageid, - arg: $.map([key.value, varname, tabindex], JSON.stringify) + arg: $.map([key.value, varname], JSON.stringify) }; cw.jqNode('div:value-subject:' + varname).loadxhtml(AJAX_BASE_URL, args, 'post'); } diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/formfields.py --- a/cubicweb/web/formfields.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/formfields.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -111,6 +111,7 @@ result += sorted(partresult) return result + _MARKER = nullobject() @@ -361,7 +362,6 @@ if callable(self.value): return self.value(form, self) return self.value - formattr = '%s_%s_default' % (self.role, self.name) if self.eidparam and self.role is not None: if form._cw.vreg.schema.rschema(self.name).final: return form.edited_entity.e_schema.default(self.name) @@ -1237,8 +1237,19 @@ kwargs.setdefault('label', (eschema.type, rschema.type)) kwargs.setdefault('help', rdef.description) if rschema.final: - fieldclass = FIELDS[targetschema] - if fieldclass is StringField: + fieldclass = kwargs.pop('fieldclass', FIELDS[targetschema]) + if issubclass(fieldclass, FileField): + if req: + aff_kwargs = req.vreg['uicfg'].select('autoform_field_kwargs', req) + else: + aff_kwargs = _AFF_KWARGS + for metadata in KNOWN_METAATTRIBUTES: + metaschema = eschema.has_metadata(rschema, metadata) + if metaschema is not None: + metakwargs = aff_kwargs.etype_get(eschema, metaschema, 'subject') + kwargs['%s_field' % metadata] = guess_field(eschema, metaschema, + req=req, **metakwargs) + elif issubclass(fieldclass, StringField): if eschema.has_metadata(rschema, 'format'): # use RichTextField instead of StringField if the attribute has # a "format" metadata. But getting information from constraints @@ -1255,18 +1266,6 @@ for cstr in rdef.constraints: if isinstance(cstr, SizeConstraint) and cstr.max is not None: kwargs['max_length'] = cstr.max - return StringField(**kwargs) - if fieldclass is FileField: - if req: - aff_kwargs = req.vreg['uicfg'].select('autoform_field_kwargs', req) - else: - aff_kwargs = _AFF_KWARGS - for metadata in KNOWN_METAATTRIBUTES: - metaschema = eschema.has_metadata(rschema, metadata) - if metaschema is not None: - metakwargs = aff_kwargs.etype_get(eschema, metaschema, 'subject') - kwargs['%s_field' % metadata] = guess_field(eschema, metaschema, - req=req, **metakwargs) return fieldclass(**kwargs) return RelationField.fromcardinality(card, **kwargs) diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/formwidgets.py --- a/cubicweb/web/formwidgets.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/formwidgets.py Fri Feb 03 13:37:32 2017 +0100 @@ -125,9 +125,6 @@ :attr:`setdomid` flag telling if HTML DOM identifier should be set on input. - :attr:`settabindex` - flag telling if HTML tabindex attribute of inputs should be set. - :attr:`suffix` string to use a suffix when generating input, to ease usage as a sub-widgets (eg widget used by another widget) @@ -157,21 +154,17 @@ needs_js = () needs_css = () setdomid = True - settabindex = True suffix = None # does this widget expect a vocabulary vocabulary_widget = False - def __init__(self, attrs=None, setdomid=None, settabindex=None, suffix=None): + def __init__(self, attrs=None, setdomid=None, suffix=None): if attrs is None: attrs = {} self.attrs = attrs if setdomid is not None: # override class's default value self.setdomid = setdomid - if settabindex is not None: - # override class's default value - self.settabindex = settabindex if suffix is not None: self.suffix = suffix @@ -202,14 +195,11 @@ def attributes(self, form, field): """Return HTML attributes for the widget, automatically setting DOM - identifier and tabindex when desired (see :attr:`setdomid` and - :attr:`settabindex` attributes) + identifier when desired (see :attr:`setdomid` attribute) """ attrs = dict(self.attrs) if self.setdomid: attrs['id'] = field.dom_id(form, self.suffix) - if self.settabindex and 'tabindex' not in attrs: - attrs['tabindex'] = form._cw.next_tabindex() if 'placeholder' in attrs: attrs['placeholder'] = form._cw._(attrs['placeholder']) return attrs @@ -386,7 +376,6 @@ """ type = 'hidden' setdomid = False # by default, don't set id attribute on hidden input - settabindex = False class ButtonInput(Input): @@ -682,9 +671,12 @@ choose a date anterior(/posterior) to this DatePicker. example: - start and end are two JQueryDatePicker and start must always be before end + + start and end are two JQueryDatePicker and start must always be before end:: + affk.set_field_kwargs(etype, 'start_date', widget=JQueryDatePicker(min_of='end_date')) affk.set_field_kwargs(etype, 'end_date', widget=JQueryDatePicker(max_of='start_date')) + That way, on change of end(/start) value a new max(/min) will be set for start(/end) The invalid dates will be gray colored in the datepicker """ @@ -1000,8 +992,6 @@ attrs = dict(self.attrs) if self.setdomid: attrs['id'] = field.dom_id(form) - if self.settabindex and 'tabindex' not in attrs: - attrs['tabindex'] = req.next_tabindex() # ensure something is rendered inputs = [u'
', req._('i18n_bookmark_url_path'), @@ -1012,8 +1002,6 @@ u''] if self.setdomid: attrs['id'] = field.dom_id(form, 'fqs') - if self.settabindex: - attrs['tabindex'] = req.next_tabindex() attrs.setdefault('cols', 60) attrs.setdefault('onkeyup', 'autogrow(this)') inputs += [tags.textarea(fqs, name=fqsqname, **attrs), @@ -1061,9 +1049,9 @@ css_class = 'validateButton' def __init__(self, label=stdmsgs.BUTTON_OK, attrs=None, - setdomid=None, settabindex=None, + setdomid=None, name='', value='', onclick=None, cwaction=None): - super(Button, self).__init__(attrs, setdomid, settabindex) + super(Button, self).__init__(attrs, setdomid) if isinstance(label, tuple): self.label = label[0] self.icon = label[1] @@ -1089,8 +1077,6 @@ attrs['name'] = self.name if self.setdomid: attrs['id'] = self.name - if self.settabindex and 'tabindex' not in attrs: - attrs['tabindex'] = form._cw.next_tabindex() if self.icon: img = tags.img(src=form._cw.uiprops[self.icon], alt=self.icon) else: diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/request.py --- a/cubicweb/web/request.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/request.py Fri Feb 03 13:37:32 2017 +0100 @@ -105,28 +105,19 @@ """ ajax_request = False # to be set to True by ajax controllers - def __init__(self, vreg, https=False, form=None, headers=None): + def __init__(self, vreg, form=None, headers=None): """ :vreg: Vregistry, - :https: boolean, s this a https request :form: Forms value :headers: dict, request header """ super(_CubicWebRequestBase, self).__init__(vreg) - #: (Boolean) Is this an https request. - self.https = https - #: User interface property (vary with https) (see :ref:`uiprops`) + #: User interface property (see :ref:`uiprops`) self.uiprops = None - #: url for serving datadir (vary with https) (see :ref:`resources`) + #: url for serving datadir (see :ref:`resources`) self.datadir_url = None - if https and vreg.config.https_uiprops is not None: - self.uiprops = vreg.config.https_uiprops - else: - self.uiprops = vreg.config.uiprops - if https and vreg.config.https_datadir_url is not None: - self.datadir_url = vreg.config.https_datadir_url - else: - self.datadir_url = vreg.config.datadir_url + self.uiprops = vreg.config.uiprops + self.datadir_url = vreg.config.datadir_url #: enable UStringIO's write tracing self.tracehtml = False if vreg.config.debugmode: @@ -179,22 +170,6 @@ self.ajax_request = value json_request = property(_get_json_request, _set_json_request) - def _base_url(self, secure=None): - """return the root url of the instance - - secure = False -> base-url - secure = None -> https-url if req.https - secure = True -> https if it exist - """ - if secure is None: - secure = self.https - base_url = None - if secure: - base_url = self.vreg.config.get('https-url') - if base_url is None: - base_url = super(_CubicWebRequestBase, self)._base_url() - return base_url - @property def authmode(self): """Authentification mode of the instance @@ -215,13 +190,6 @@ """ return self.set_varmaker() - def next_tabindex(self): - nextfunc = self.get_page_data('nexttabfunc') - if nextfunc is None: - nextfunc = Counter(1) - self.set_page_data('nexttabfunc', nextfunc) - return nextfunc() - def set_varmaker(self): varmaker = self.get_page_data('rql_varmaker') if varmaker is None: @@ -959,7 +927,7 @@ cnx = None session = None - def __init__(self, vreg, https=False, form=None, headers={}): + def __init__(self, vreg, form=None, headers={}): """""" self.vreg = vreg try: @@ -967,8 +935,7 @@ self.translations = vreg.config.translations except AttributeError: self.translations = {} - super(ConnectionCubicWebRequestBase, self).__init__(vreg, https=https, - form=form, headers=headers) + super(ConnectionCubicWebRequestBase, self).__init__(vreg, form=form, headers=headers) self.session = _MockAnonymousSession() self.cnx = self.user = _NeedAuthAccessMock() @@ -1016,11 +983,8 @@ def cached_entities(self): return self.transaction_data.get('req_ecache', {}).values() - def drop_entity_cache(self, eid=None): - if eid is None: - self.transaction_data.pop('req_ecache', None) - else: - del self.transaction_data['req_ecache'][eid] + def drop_entity_cache(self): + self.transaction_data.pop('req_ecache', None) CubicWebRequestBase = ConnectionCubicWebRequestBase diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/test/unittest_application.py --- a/cubicweb/web/test/unittest_application.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/test/unittest_application.py Fri Feb 03 13:37:32 2017 +0100 @@ -503,7 +503,7 @@ self.assertTrue(cnx.find('Directory', eid=subd.eid)) self.assertTrue(cnx.find('Filesystem', eid=fs.eid)) self.assertEqual(cnx.find('Directory', eid=subd.eid).one().parent, - [topd,]) + (topd,)) def test_subject_mixed_composite_subentity_removal_2(self): """Editcontroller: detaching several subentities respects each rdef's @@ -542,7 +542,7 @@ self.assertTrue(cnx.find('Directory', eid=subd.eid)) self.assertTrue(cnx.find('Filesystem', eid=fs.eid)) self.assertEqual(cnx.find('Directory', eid=subd.eid).one().parent, - [topd,]) + (topd,)) def test_object_mixed_composite_subentity_removal_2(self): """Editcontroller: detaching several subentities respects each rdef's diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/test/unittest_form.py --- a/cubicweb/web/test/unittest_form.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/test/unittest_form.py Fri Feb 03 13:37:32 2017 +0100 @@ -200,20 +200,20 @@ def test_richtextfield_1(self): with self.admin_access.web_request() as req: req.use_fckeditor = lambda: False - self._test_richtextfield(req, ''' ''' + ('\n' if HAS_TAL else '') + ''' -''') +''') def test_richtextfield_2(self): with self.admin_access.web_request() as req: req.use_fckeditor = lambda: True - self._test_richtextfield(req, '') + self._test_richtextfield(req, '') def test_filefield(self): @@ -229,11 +229,11 @@ data=Binary(b'new widgets system')) form = FFForm(req, redirect_path='perdu.com', entity=file) self.assertMultiLineEqual(self._render_entity_field(req, 'data', form), - ''' + ''' show advanced fields
@@ -253,17 +253,17 @@ data=Binary(b'new widgets system')) form = EFFForm(req, redirect_path='perdu.com', entity=file) self.assertMultiLineEqual(self._render_entity_field(req, 'data', form), - ''' + ''' show advanced fields
detach attached file

You can either submit a new file using the browse button above, or choose to remove already uploaded file by checking the "detach attached file" check-box, or edit file content online with the widget below.

-''' % {'eid': file.eid}) +''' % {'eid': file.eid}) def _modified_tzdatenaiss(self, eid, date_and_time_str=None): ctx = {} @@ -303,9 +303,9 @@ with self.admin_access.web_request() as req: form = PFForm(req, redirect_path='perdu.com', entity=req.user) self.assertMultiLineEqual(self._render_entity_field(req, 'upassword', form), - ''' + '''
- +   confirm password''' % {'eid': req.user.eid}) diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/test/unittest_formwidgets.py --- a/cubicweb/web/test/unittest_formwidgets.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/test/unittest_formwidgets.py Fri Feb 03 13:37:32 2017 +0100 @@ -38,7 +38,7 @@ def test_bitselect_widget(self): field = formfields.guess_field(self.schema['CWAttribute'], self.schema['ordernum']) field.choices = [('un', '1',), ('deux', '2',)] - widget = formwidgets.BitSelect(settabindex=False) + widget = formwidgets.BitSelect() req = fake.FakeRequest(form={'ordernum-subject:A': ['1', '2']}) form = mock(_cw=req, formvalues={}, edited_entity=mock(eid='A'), form_previous_values=()) @@ -62,7 +62,7 @@ field = form.field_by_name('bool') widget = field.widget self.assertMultiLineEqual(widget._render(form, field, None), - '') diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/test/unittest_http_headers.py --- a/cubicweb/web/test/unittest_http_headers.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/test/unittest_http_headers.py Fri Feb 03 13:37:32 2017 +0100 @@ -13,6 +13,7 @@ with self.assertRaises(ValueError): http_headers.generateTrueFalse('any value') + if __name__ == '__main__': from unittest import main main() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/test/unittest_reledit.py --- a/cubicweb/web/test/unittest_reledit.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/test/unittest_reledit.py Fri Feb 03 13:37:32 2017 +0100 @@ -73,13 +73,13 @@
- +
- - + +
@@ -108,23 +108,23 @@
- +
- +
- +
- - + +
@@ -152,7 +152,7 @@
- @@ -160,8 +160,8 @@
- - + +
diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/test/unittest_request.py --- a/cubicweb/web/test/unittest_request.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/test/unittest_request.py Fri Feb 03 13:37:32 2017 +0100 @@ -71,28 +71,13 @@ class WebRequestTC(unittest.TestCase): - def test_base_url(self): - dummy_vreg = FakeCWRegistryStore(FakeConfig(), initlog=False) - dummy_vreg.config['base-url'] = 'http://babar.com/' - dummy_vreg.config['https-url'] = 'https://toto.com/' - - req = CubicWebRequestBase(dummy_vreg, https=False) - self.assertEqual('http://babar.com/', req.base_url()) - self.assertEqual('http://babar.com/', req.base_url(False)) - self.assertEqual('https://toto.com/', req.base_url(True)) - - req = CubicWebRequestBase(dummy_vreg, https=True) - self.assertEqual('https://toto.com/', req.base_url()) - self.assertEqual('http://babar.com/', req.base_url(False)) - self.assertEqual('https://toto.com/', req.base_url(True)) - def test_negotiated_language(self): vreg = FakeCWRegistryStore(FakeConfig(), initlog=False) vreg.config.translations = {'fr': (None, None), 'en': (None, None)} headers = { 'Accept-Language': 'fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3', } - req = CubicWebRequestBase(vreg, https=False, headers=headers) + req = CubicWebRequestBase(vreg, headers=headers) self.assertEqual(req.negotiated_language(), 'fr') def test_build_url_language_from_url(self): @@ -100,7 +85,7 @@ vreg.config['base-url'] = 'http://testing.fr/cubicweb/' vreg.config['language-mode'] = 'url-prefix' vreg.config.translations['fr'] = text_type, text_type - req = CubicWebRequestBase(vreg, https=False) + req = CubicWebRequestBase(vreg) # Override from_controller to avoid getting into relative_path method, # which is not implemented in CubicWebRequestBase. req.from_controller = lambda : 'not view' diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/test/unittest_uicfg.py --- a/cubicweb/web/test/unittest_uicfg.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/test/unittest_uicfg.py Fri Feb 03 13:37:32 2017 +0100 @@ -16,6 +16,8 @@ # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . import copy +import warnings + from logilab.common.testlib import tag from cubicweb.devtools.testlib import CubicWebTC from cubicweb.web import uihelper, formwidgets as fwdgs @@ -70,7 +72,10 @@ def test_uihelper_set_fields_order(self): afk_get = uicfg.autoform_field_kwargs.get self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {}) - uihelper.set_fields_order('CWUser', ('login', 'firstname', 'surname')) + with warnings.catch_warnings(record=True) as w: + uihelper.set_fields_order('CWUser', ('login', 'firstname', 'surname')) + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {'order': 1}) @tag('uicfg', 'order', 'func') @@ -86,7 +91,10 @@ afk_get = uicfg.autoform_field_kwargs.get self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {}) wdg = fwdgs.TextInput({'size': 30}) - uihelper.set_field_kwargs('CWUser', 'firstname', widget=wdg) + with warnings.catch_warnings(record=True) as w: + uihelper.set_field_kwargs('CWUser', 'firstname', widget=wdg) + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) self.assertEqual(afk_get('CWUser', 'firstname', 'String', 'subject'), {'widget': wdg}) @tag('uihelper', 'hidden', 'func') @@ -95,11 +103,17 @@ section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject') self.assertCountEqual(section_conf, ['main_attributes', 'muledit_attributes']) # hide field in main form - uihelper.hide_fields('CWUser', ('login', 'in_group')) + with warnings.catch_warnings(record=True) as w: + uihelper.hide_fields('CWUser', ('login', 'in_group')) + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject') self.assertCountEqual(section_conf, ['main_hidden', 'muledit_attributes']) # hide field in muledit form - uihelper.hide_fields('CWUser', ('login', 'in_group'), formtype='muledit') + with warnings.catch_warnings(record=True) as w: + uihelper.hide_fields('CWUser', ('login', 'in_group'), formtype='muledit') + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) section_conf = uicfg.autoform_section.get('CWUser', 'in_group', '*', 'subject') self.assertCountEqual(section_conf, ['main_hidden', 'muledit_hidden']) diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/test/unittest_views_basecontrollers.py --- a/cubicweb/web/test/unittest_views_basecontrollers.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/test/unittest_views_basecontrollers.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -27,22 +27,20 @@ from logilab.common.testlib import unittest_main from logilab.common.decorators import monkeypatch -from cubicweb import Binary, NoSelectableObject, ValidationError, AuthenticationError +from cubicweb import Binary, NoSelectableObject, ValidationError, transaction as tx from cubicweb.schema import RRQLExpression +from cubicweb.predicates import is_instance from cubicweb.devtools.testlib import CubicWebTC from cubicweb.devtools.webtest import CubicWebTestTC from cubicweb.devtools.httptest import CubicWebServerTC from cubicweb.utils import json_dumps from cubicweb.uilib import rql_for_eid -from cubicweb.web import Redirect, RemoteCallFailed, http_headers -import cubicweb.server.session -from cubicweb.server.session import Connection +from cubicweb.web import Redirect, RemoteCallFailed, http_headers, formfields as ff from cubicweb.web.views.autoform import get_pending_inserts, get_pending_deletes from cubicweb.web.views.basecontrollers import JSonController, xhtmlize, jsonize from cubicweb.web.views.ajaxcontroller import ajaxfunc, AjaxFunction -import cubicweb.transaction as tx +from cubicweb.server.session import Connection from cubicweb.server.hook import Hook, Operation -from cubicweb.predicates import is_instance class ViewControllerTC(CubicWebTestTC): @@ -617,6 +615,58 @@ finally: blogentry.__class__.cw_skip_copy_for = [] + def test_avoid_multiple_process_posted(self): + # test that when some entity is being created and data include non-inlined relations, the + # values for this relation are stored for later usage, without calling twice field's + # process_form method, which may be unexpected for custom fields + + orig_process_posted = ff.RelationField.process_posted + + def count_process_posted(self, form): + res = list(orig_process_posted(self, form)) + nb_process_posted_calls[0] += 1 + return res + + ff.RelationField.process_posted = count_process_posted + + try: + with self.admin_access.web_request() as req: + gueid = req.execute('CWGroup G WHERE G name "users"')[0][0] + req.form = { + 'eid': 'X', + '__type:X': 'CWUser', + '_cw_entity_fields:X': 'login-subject,upassword-subject,in_group-subject', + 'login-subject:X': u'adim', + 'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto', + 'in_group-subject:X': repr(gueid), + } + nb_process_posted_calls = [0] + self.expect_redirect_handle_request(req, 'edit') + self.assertEqual(nb_process_posted_calls[0], 1) + user = req.find('CWUser', login=u'adim').one() + self.assertEqual(set(g.eid for g in user.in_group), set([gueid])) + req.form = { + 'eid': ['X', 'Y'], + '__type:X': 'CWUser', + '_cw_entity_fields:X': 'login-subject,upassword-subject,in_group-subject', + 'login-subject:X': u'dlax', + 'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto', + 'in_group-subject:X': repr(gueid), + + '__type:Y': 'EmailAddress', + '_cw_entity_fields:Y': 'address-subject,use_email-object', + 'address-subject:Y': u'dlax@cw.org', + 'use_email-object:Y': 'X', + } + nb_process_posted_calls = [0] + self.expect_redirect_handle_request(req, 'edit') + self.assertEqual(nb_process_posted_calls[0], 3) # 3 = 1 (in_group) + 2 (use_email) + user = req.find('CWUser', login=u'dlax').one() + self.assertEqual(set(e.address for e in user.use_email), set(['dlax@cw.org'])) + + finally: + ff.RelationField.process_posted = orig_process_posted + def test_nonregr_eetype_etype_editing(self): """non-regression test checking that a manager user can edit a CWEType entity """ @@ -669,7 +719,6 @@ self.assertEqual(e.title, '"13:03:40"') self.assertEqual(e.content, '"13:03:43"') - def test_nonregr_multiple_empty_email_addr(self): with self.admin_access.web_request() as req: gueid = req.execute('CWGroup G WHERE G name "users"')[0][0] diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/test/unittest_views_baseviews.py --- a/cubicweb/web/test/unittest_views_baseviews.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/test/unittest_views_baseviews.py Fri Feb 03 13:37:32 2017 +0100 @@ -156,5 +156,138 @@ b''], source_lines[:3]) +class BaseViewsTC(CubicWebTC): + + def test_null(self): + with self.admin_access.web_request() as req: + rset = req.execute('Any X WHERE X login "admin"') + result = req.view('null', rset) + self.assertEqual(result, u'') + + def test_final(self): + with self.admin_access.web_request() as req: + rset = req.execute('Any ""') + result = req.view('final', rset) + self.assertEqual(result, u'<script></script>') + + def test_incontext(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + result = entity.view('incontext') + expected = (u'' + u'<script></script>' % entity.eid) + self.assertEqual(result, expected) + + def test_outofcontext(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + result = entity.view('outofcontext') + expect = (u'' + u'<script></script>' % entity.eid) + self.assertEqual(result, expect) + + def test_outofcontext(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + result = entity.view('oneline') + expect = (u'' + u'<script></script>' % entity.eid) + self.assertEqual(result, expect) + + def test_text(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + result = entity.view('text') + self.assertEqual(result, u'') + + def test_textincontext(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + result = entity.view('textincontext') + self.assertEqual(result, u'') + + def test_textoutofcontext(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + result = entity.view('textoutofcontext') + self.assertEqual(result, u'') + + def test_list(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + rset = req.execute('Any X WHERE X is CWUser') + result = req.view('list', rset) + expected = u''' +''' % entity.eid + self.assertEqual(result, expected) + + def test_simplelist(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + rset = req.execute('Any X WHERE X is CWUser') + result = req.view('simplelist', rset) + expected = ( + u'' + u'
' + u'admin
' + u'
' + u'anon
' + % entity.eid + ) + self.assertEqual(result, expected) + + def test_sameetypelist(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + rset = req.execute('Any X WHERE X is CWUser') + result = req.view('sameetypelist', rset) + expected = ( + u'

CWUser_plural

' + u'' + u'
' + u'admin
' + u'
' + u'anon
' + % entity.eid + ) + self.assertEqual(expected, result) + + def test_sameetypelist(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + rset = req.execute('Any X WHERE X is CWUser') + result = req.view('csv', rset) + expected = ( + u'<script></script>, ' + u'admin, ' + u'anon' + % entity.eid + ) + self.assertEqual(result, expected) + + def test_metadata(self): + with self.admin_access.web_request() as req: + entity = req.create_entity('CWUser', login=u'', upassword=u'toto') + entity.cw_set(creation_date=u'2000-01-01 00:00:00') + entity.cw_set(modification_date=u'2015-01-01 00:00:00') + result = entity.view('metadata') + expected = ( + u'
CWUser #%d - latest update on' + u' 2015/01/01,' + u' created on' + u' 2000/01/01
' + % entity.eid + ) + self.assertEqual(result, expected) + + if __name__ == '__main__': unittest_main() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/test/unittest_views_forms.py --- a/cubicweb/web/test/unittest_views_forms.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/test/unittest_views_forms.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2014-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -20,6 +20,8 @@ from cubicweb.devtools.testlib import CubicWebTC from cubicweb.web.views.autoform import InlinedFormField +from cubicweb.web.views.forms import EntityFieldsForm + class InlinedFormTC(CubicWebTC): @@ -68,7 +70,21 @@ InlinedFormField(view=formview)]) self.assertTrue(formview._get_removejs()) + def test_field_by_name_consider_aff(self): + class MyField(object): + def __init__(self, *args, **kwargs): + pass + + EntityFieldsForm.uicfg_aff.tag_attribute(('CWUser', 'firstname'), MyField) + try: + with self.admin_access.web_request() as req: + form = req.vreg['forms'].select('base', req, entity=req.user) + self.assertIsInstance(form.field_by_name('firstname', 'subject', req.user.e_schema), + MyField) + finally: + EntityFieldsForm.uicfg_aff.del_rtag('CWUser', 'firstname', '*', 'subject') + if __name__ == '__main__': - from logilab.common.testlib import unittest_main - unittest_main() + import unittest + unittest.main() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/actions.py --- a/cubicweb/web/views/actions.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/actions.py Fri Feb 03 13:37:32 2017 +0100 @@ -48,7 +48,7 @@ # display action anyway form = entity._cw.vreg['forms'].select('edition', entity._cw, entity=entity, mainform=False) - for dummy in form.editable_relations(): + for dummy in form.iter_editable_relations(): return 1 for dummy in form.inlined_form_views(): return 1 diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/autoform.py --- a/cubicweb/web/views/autoform.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/autoform.py Fri Feb 03 13:37:32 2017 +0100 @@ -565,9 +565,9 @@ w(u'') w(u'' % eid) w(u'') - w(u' + ''' % (not self.cw_propval('visible') and 'hidden' or '', - req.build_url('view'), xml_escape(rql), req._('full text or RQL query'), req.next_tabindex())) + req.build_url('view'), xml_escape(rql), req._('full text or RQL query'))) if req.search_state[0] != 'normal': self.w(u'' % ':'.join(req.search_state[1])) @@ -169,7 +169,8 @@ def render(self, w): # display useractions and siteactions self._cw.add_css('cubicweb.pictograms.css') - actions = self._cw.vreg['actions'].possible_actions(self._cw, rset=self.cw_rset) + actions = self._cw.vreg['actions'].possible_actions(self._cw, rset=self.cw_rset, + view=self.cw_extra_kwargs['view']) box = MenuWidget('', 'userActionsBox', _class='', islist=False) menu = PopupBoxMenu(self._cw.user.login, isitem=False, link_class='icon-user') box.append(menu) diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/basetemplates.py --- a/cubicweb/web/views/basetemplates.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/basetemplates.py Fri Feb 03 13:37:32 2017 +0100 @@ -18,20 +18,20 @@ """default templates for CubicWeb web client""" -from cubicweb import _ - from logilab.mtconverter import xml_escape from logilab.common.deprecation import class_renamed from logilab.common.registry import objectify_predicate from logilab.common.decorators import classproperty -from cubicweb.predicates import match_kwargs, no_cnx, anonymous_user +from cubicweb import _ +from cubicweb.predicates import match_kwargs, anonymous_user from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW, StartupView from cubicweb.utils import UStringIO from cubicweb.schema import display_name -from cubicweb.web import component, formfields as ff, formwidgets as fw +from cubicweb.web import formfields as ff, formwidgets as fw from cubicweb.web.views import forms + # main templates ############################################################## class LogInOutTemplate(MainTemplate): @@ -92,6 +92,7 @@ if req.form.get('__modal', None): return 1 + @objectify_predicate def templatable_view(cls, req, rset, *args, **kwargs): view = kwargs.pop('view', None) @@ -176,7 +177,6 @@ def template_html_header(self, content_type, page_title, additional_headers=()): w = self.whead - lang = self._cw.lang self.write_doctype() self._cw.html_headers.define_var('BASE_URL', self._cw.base_url()) self._cw.html_headers.define_var('DATA_URL', self._cw.datadir_url) @@ -208,14 +208,13 @@ self.w(u'\n') self.nav_column(view, 'right') self.w(u'\n') - self.wview('footer', rset=self.cw_rset) + self.wview('footer', rset=self.cw_rset, view=view) self.w(u'') def nav_column(self, view, context): boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects( self._cw, rset=self.cw_rset, view=view, context=context)) if boxes: - getlayout = self._cw.vreg['components'].select self.w(u'') + class BaseLogForm(forms.FieldsForm): """Abstract Base login form to be used by any login form """ @@ -461,7 +461,7 @@ fw.ResetButton(label=_('cancel'), attrs={'class': 'loginButton', 'onclick': onclick}),] - ## Can't shortcut next access because __dict__ is a "dictproxy" which + ## Can't shortcut next access because __dict__ is a "dictproxy" which ## does not support items assignement. # cls.__dict__['form_buttons'] = form_buttons return form_buttons @@ -474,9 +474,10 @@ url_args = {} if target and target != '/': url_args['postlogin_path'] = target - return self._cw.build_url('login', __secure__=True, **url_args) + return self._cw.build_url('login', **url_args) return super(BaseLogForm, self).form_action() + class LogForm(BaseLogForm): """Simple login form that send username and password """ @@ -488,7 +489,7 @@ __password = ff.StringField('__password', label=_('password'), widget=fw.PasswordSingleInput({'class': 'data'})) - onclick_args = ('popupLoginBox', '__login') + onclick_args = ('popupLoginBox', '__login') class LogFormView(View): @@ -531,4 +532,5 @@ form.render(w=self.w, table_class='', display_progress_div=False) cw.html_headers.add_onload('jQuery("#__login:visible").focus()') + LogFormTemplate = class_renamed('LogFormTemplate', LogFormView) diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/boxes.py --- a/cubicweb/web/views/boxes.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/boxes.py Fri Feb 03 13:37:32 2017 +0100 @@ -67,7 +67,7 @@ self._menus_by_id = {} # build list of actions actions = self._cw.vreg['actions'].possible_actions(self._cw, self.cw_rset, - **self.cw_extra_kwargs) + view=self.cw_extra_kwargs['view']) other_menu = self._get_menu('moreactions', _('more actions')) for category, defaultmenu in (('mainactions', self), ('moreactions', other_menu), @@ -138,11 +138,11 @@ order = 0 formdef = u"""
- + - +
""" @@ -155,13 +155,10 @@ rql = self._cw.form.get('rql', '') else: rql = '' - tabidx1 = self._cw.next_tabindex() - tabidx2 = self._cw.next_tabindex() w(self.formdef % {'action': self._cw.build_url('view'), 'value': xml_escape(rql), - 'id': self.cw_extra_kwargs.get('domid', 'tsearch'), - 'tabindex1': tabidx1, - 'tabindex2': tabidx2}) + 'id': self.cw_extra_kwargs.get('domid', 'tsearch') + }) # boxes disabled by default ################################################### diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/calendar.py --- a/cubicweb/web/views/calendar.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/calendar.py Fri Feb 03 13:37:32 2017 +0100 @@ -41,12 +41,17 @@ _('november'), _('december') ) +ICAL_EVENT = "event" +ICAL_TODO = "todo" class ICalendarableAdapter(EntityAdapter): __needs_bw_compat__ = True __regid__ = 'ICalendarable' __abstract__ = True + # component type + component = ICAL_EVENT + @property def start(self): """return start date""" @@ -64,7 +69,7 @@ from vobject import iCalendar class iCalView(EntityView): - """A calendar view that generates a iCalendar file (RFC 2445) + """A calendar view that generates a iCalendar file (RFC 5545) Does apply to ICalendarable compatible entities """ @@ -79,14 +84,21 @@ ical = iCalendar() for i in range(len(self.cw_rset.rows)): task = self.cw_rset.complete_entity(i, 0) - event = ical.add('vevent') - event.add('summary').value = task.dc_title() - event.add('description').value = task.dc_description() - icalendarable = task.cw_adapt_to('ICalendarable') - if icalendarable.start: - event.add('dtstart').value = icalendarable.start - if icalendarable.stop: - event.add('dtend').value = icalendarable.stop + ical_task = task.cw_adapt_to('ICalendarable') + if ical_task.component == ICAL_TODO: + elt = ical.add('vtodo') + stop_kw = "due" + else: + elt = ical.add('vevent') + stop_kw = "dtend" + elt.add('uid').value = task.absolute_url() # unique stable id + elt.add('url').value = task.absolute_url() + elt.add('summary').value = task.dc_title() + elt.add('description').value = task.dc_description() + if ical_task.start: + elt.add('dtstart').value = ical_task.start + if ical_task.stop: + elt.add(stop_kw).value = ical_task.stop buff = ical.serialize() if not isinstance(buff, unicode): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/cwproperties.py --- a/cubicweb/web/views/cwproperties.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/cwproperties.py Fri Feb 03 13:37:32 2017 +0100 @@ -315,9 +315,8 @@ def render(self, form, renderer): wdg = self.get_widget(form) # pylint: disable=E1101 - wdg.attrs['tabindex'] = form._cw.next_tabindex() - wdg.attrs['onchange'] = "javascript:setPropValueWidget('%s', %s)" % ( - form.edited_entity.eid, form._cw.next_tabindex()) + wdg.attrs['onchange'] = "javascript:setPropValueWidget('%s')" % ( + form.edited_entity.eid) return wdg.render(form, self, renderer) def vocabulary(self, form): @@ -335,10 +334,8 @@ """ widget = PlaceHolderWidget - def render(self, form, renderer=None, tabindex=None): + def render(self, form, renderer=None): wdg = self.get_widget(form) - if tabindex is not None: - wdg.attrs['tabindex'] = tabindex return wdg.render(form, self, renderer) def form_init(self, form): @@ -422,7 +419,7 @@ @ajaxfunc(output_type='xhtml') -def prop_widget(self, propkey, varname, tabindex=None): +def prop_widget(self, propkey, varname): """specific method for CWProperty handling""" entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw) entity.eid = varname @@ -431,7 +428,7 @@ form.build_context() vfield = form.field_by_name('value', 'subject') renderer = formrenderers.FormRenderer(self._cw) - return vfield.render(form, renderer, tabindex=tabindex) \ + return vfield.render(form, renderer) \ + renderer.render_help(form, vfield) _afs = uicfg.autoform_section diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/editcontroller.py --- a/cubicweb/web/views/editcontroller.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/editcontroller.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -17,8 +17,6 @@ # with CubicWeb. If not, see . """The edit controller, automatically handling entity form submitting""" - - from warnings import warn from collections import defaultdict @@ -26,16 +24,14 @@ from six import text_type -from logilab.common.deprecation import deprecated from logilab.common.graph import ordered_nodes from rql.utils import rqlvar_maker -from cubicweb import _, Binary, ValidationError, UnknownEid +from cubicweb import _, ValidationError, UnknownEid from cubicweb.view import EntityAdapter from cubicweb.predicates import is_instance -from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, - ProcessFormError) +from cubicweb.web import RequestError, NothingToEdit, ProcessFormError from cubicweb.web.views import basecontrollers, autoform @@ -74,6 +70,7 @@ except (ValueError, TypeError): return eid + class RqlQuery(object): def __init__(self): self.edited = [] @@ -190,15 +187,16 @@ req.transaction_data['__maineid'] = form['__maineid'] # no specific action, generic edition self._to_create = req.data['eidmap'] = {} - # those two data variables are used to handle relation from/to entities + # those three data variables are used to handle relation from/to entities # which doesn't exist at time where the entity is edited and that # deserves special treatment req.data['pending_inlined'] = defaultdict(set) req.data['pending_others'] = set() req.data['pending_composite_delete'] = set() + req.data['pending_values'] = dict() try: for formparams in self._ordered_formparams(): - eid = self.edit_entity(formparams) + self.edit_entity(formparams) except (RequestError, NothingToEdit) as ex: if '__linkto' in req.form and 'eid' in req.form: self.execute_linkto() @@ -208,10 +206,20 @@ # treated now (pop to ensure there are no attempt to add new ones) pending_inlined = req.data.pop('pending_inlined') assert not pending_inlined, pending_inlined + pending_values = req.data.pop('pending_values') # handle all other remaining relations now while req.data['pending_others']: form_, field = req.data['pending_others'].pop() - self.handle_formfield(form_, field) + # attempt to retrieve values and original values if they have already gone through + # handle_formfield (may not if there has been some not yet known eid at the first + # processing round). In the later case we've to go back through handle_formfield. + try: + values, origvalues = pending_values.pop((form_, field)) + except KeyError: + self.handle_formfield(form_, field) + else: + self.handle_relation(form_, field, values, origvalues) + assert not pending_values, 'unexpected remaining pending values %s' % pending_values del req.data['pending_others'] # then execute rql to set all relations for querydef in self.relations_rql: @@ -236,7 +244,7 @@ neweid = entity.eid except ValidationError as ex: self._to_create[eid] = ex.entity - if self._cw.ajax_request: # XXX (syt) why? + if self._cw.ajax_request: # XXX (syt) why? ex.entity = eid raise self._to_create[eid] = neweid @@ -268,7 +276,7 @@ form = req.vreg['forms'].select(formid, req, entity=entity) eid = form.actual_eid(entity.eid) editedfields = formparams['_cw_entity_fields'] - form.formvalues = {} # init fields value cache + form.formvalues = {} # init fields value cache for field in form.iter_modified_fields(editedfields, entity): self.handle_formfield(form, field, rqlquery) # if there are some inlined field which were waiting for this entity's @@ -279,9 +287,9 @@ if self.errors: errors = dict((f.role_name(), text_type(ex)) for f, ex in self.errors) raise ValidationError(valerror_eid(entity.eid), errors) - if eid is None: # creation or copy + if eid is None: # creation or copy entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery) - elif rqlquery.edited: # edition of an existant entity + elif rqlquery.edited: # edition of an existant entity self.check_concurrent_edition(formparams, eid) self._update_entity(eid, rqlquery) else: @@ -294,7 +302,7 @@ autoform.delete_relations(req, todelete) if '__cloned_eid' in formparams: entity.copy_relations(int(formparams['__cloned_eid'])) - if is_main_entity: # only execute linkto for the main entity + if is_main_entity: # only execute linkto for the main entity self.execute_linkto(entity.eid) return eid @@ -303,10 +311,9 @@ eschema = entity.e_schema try: for field, value in field.process_posted(form): - if not ( - (field.role == 'subject' and field.name in eschema.subjrels) - or - (field.role == 'object' and field.name in eschema.objrels)): + if not ((field.role == 'subject' and field.name in eschema.subjrels) + or + (field.role == 'object' and field.name in eschema.objrels)): continue rschema = self._cw.vreg.schema.rschema(field.name) @@ -315,11 +322,11 @@ continue if entity.has_eid(): - origvalues = set(data[0] for data in entity.related(field.name, field.role).rows) + origvalues = set(row[0] for row in entity.related(field.name, field.role).rows) else: origvalues = set() if value is None or value == origvalues: - continue # not edited / not modified / to do later + continue # not edited / not modified / to do later unlinked_eids = origvalues - value @@ -333,7 +340,8 @@ elif form.edited_entity.has_eid(): self.handle_relation(form, field, value, origvalues) else: - form._cw.data['pending_others'].add( (form, field) ) + form._cw.data['pending_others'].add((form, field)) + form._cw.data['pending_values'][(form, field)] = (value, origvalues) except ProcessFormError as exc: self.errors.append((field, exc)) @@ -387,15 +395,10 @@ def handle_relation(self, form, field, values, origvalues): """handle edition for the (rschema, x) relation of the given entity """ - etype = form.edited_entity.e_schema rschema = self._cw.vreg.schema.rschema(field.name) if field.role == 'subject': - desttype = rschema.objects(etype)[0] - card = rschema.rdef(etype, desttype).cardinality[0] subjvar, objvar = 'X', 'Y' else: - desttype = rschema.subjects(etype)[0] - card = rschema.rdef(desttype, etype).cardinality[1] subjvar, objvar = 'Y', 'X' eid = form.edited_entity.eid if field.role == 'object' or not rschema.inlined or not values: @@ -419,7 +422,7 @@ for eid, etype in eidtypes: entity = self._cw.entity_from_eid(eid, etype) path, params = entity.cw_adapt_to('IEditControl').after_deletion_path() - redirect_info.add( (path, tuple(params.items())) ) + redirect_info.add((path, tuple(params.items()))) entity.cw_delete() if len(redirect_info) > 1: # In the face of ambiguity, refuse the temptation to guess. @@ -431,7 +434,6 @@ else: self._cw.set_message(self._cw._('entity deleted')) - def check_concurrent_edition(self, formparams, eid): req = self._cw try: @@ -446,7 +448,7 @@ msg = _("Entity %(eid)s has changed since you started to edit it." " Reload the page and reapply your changes.") # ... this is why we pass the formats' dict as a third argument. - raise ValidationError(eid, {None: msg}, {'eid' : eid}) + raise ValidationError(eid, {None: msg}, {'eid': eid}) def _action_apply(self): self._default_publish() diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/forms.py --- a/cubicweb/web/views/forms.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/forms.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -42,9 +42,6 @@ but you'll use this one rarely. """ - - - import time import inspect @@ -177,8 +174,10 @@ return self._onsubmit except AttributeError: return "return freezeFormButtons('%(domid)s');" % dictattr(self) + def _set_onsubmit(self, value): self._onsubmit = value + onsubmit = property(_get_onsubmit, _set_onsubmit) def add_media(self): @@ -210,6 +209,7 @@ rset=self.cw_rset, row=self.cw_row, col=self.cw_col or 0) formvalues = None + def build_context(self, formvalues=None): """build form context values (the .context attribute which is a dictionary with field instance as key associated to a dictionary @@ -217,7 +217,7 @@ a string). """ if self.formvalues is not None: - return # already built + return # already built self.formvalues = formvalues or {} # use a copy in case fields are modified while context is built (eg # __linkto handling for instance) @@ -239,6 +239,7 @@ eidparam=True) _default_form_action_path = 'edit' + def form_action(self): action = self.action if action is None: @@ -256,7 +257,7 @@ editedfields = self._cw.form['_cw_fields'] except KeyError: raise RequestError(self._cw._('no edited fields specified')) - entityform = entity and len(inspect.getargspec(self.field_by_name)) == 4 # XXX + entityform = entity and len(inspect.getargspec(self.field_by_name)) == 4 # XXX for editedfield in splitstrip(editedfields): try: name, role = editedfield.split('-') @@ -275,7 +276,7 @@ will return a dictionary with field names as key and typed value as associated value. """ - with tempattr(self, 'formvalues', {}): # init fields value cache + with tempattr(self, 'formvalues', {}): # init fields value cache errors = [] processed = {} for field in self.iter_modified_fields(): @@ -317,16 +318,16 @@ rschema = eschema.schema.rschema(name) # XXX use a sample target type. Document this. tschemas = rschema.targets(eschema, role) - fieldcls = cls_or_self.uicfg_aff.etype_get( + fieldclass = cls_or_self.uicfg_aff.etype_get( eschema, rschema, role, tschemas[0]) kwargs = cls_or_self.uicfg_affk.etype_get( eschema, rschema, role, tschemas[0]) if kwargs is None: kwargs = {} - if fieldcls: - if not isinstance(fieldcls, type): - return fieldcls # already and instance - return fieldcls(name=name, role=role, eidparam=True, **kwargs) + if fieldclass: + if not isinstance(fieldclass, type): + return fieldclass # already an instance + kwargs['fieldclass'] = fieldclass if isinstance(cls_or_self, type): req = None else: @@ -441,7 +442,7 @@ def actual_eid(self, eid): # should be either an int (existant entity) or a variable (to be # created entity) - assert eid or eid == 0, repr(eid) # 0 is a valid eid + assert eid or eid == 0, repr(eid) # 0 is a valid eid try: return int(eid) except ValueError: @@ -470,8 +471,8 @@ def build_context(self, formvalues=None): super(CompositeFormMixIn, self).build_context(formvalues) - for form in self.forms: - form.build_context(formvalues) + for form_ in self.forms: + form_.build_context(formvalues) class CompositeForm(CompositeFormMixIn, FieldsForm): @@ -479,5 +480,6 @@ at once. """ + class CompositeEntityForm(CompositeFormMixIn, EntityFieldsForm): - pass # XXX why is this class necessary? + pass # XXX why is this class necessary? diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/staticcontrollers.py --- a/cubicweb/web/views/staticcontrollers.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/staticcontrollers.py Fri Feb 03 13:37:32 2017 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -39,7 +39,6 @@ from cubicweb.web.views.urlrewrite import URLRewriter - class StaticFileController(Controller): """an abtract class to serve static file @@ -49,7 +48,7 @@ def max_age(self, path): """max cache TTL""" - return 60*60*24*7 + return 60 * 60 * 24 * 7 def static_file(self, path): """Return full content of a static file. @@ -81,7 +80,6 @@ self._cw.set_header('last-modified', generateDateTime(os.stat(path).st_mtime)) if self._cw.is_client_cache_valid(): return '' - # XXX elif uri.startswith('/https/'): uri = uri[6:] mimetype, encoding = mimetypes.guess_type(path) if mimetype is None: mimetype = 'application/octet-stream' @@ -226,11 +224,7 @@ __regid__ = 'fckeditor' def publish(self, rset=None): - config = self._cw.vreg.config - if self._cw.https: - uiprops = config.https_uiprops - else: - uiprops = config.uiprops + uiprops = self._cw.vreg.config.uiprops relpath = self.relpath if relpath.startswith('fckeditor/'): relpath = relpath[len('fckeditor/'):] @@ -248,9 +242,11 @@ relpath = self.relpath[len(self.__regid__) + 1:] return self.static_file(osp.join(staticdir, relpath)) + STATIC_CONTROLLERS = [DataController, FCKEditorController, StaticDirectoryController] + class StaticControlerRewriter(URLRewriter): """a quick and dirty rewritter in charge of server static file. @@ -267,6 +263,5 @@ else: self.debug("not a static file uri: %s", uri) raise KeyError(uri) - relpath = self._cw.relative_path(includeparams=False) self._cw.form['static_relative_path'] = self._cw.relative_path(includeparams=True) return ctrl.__regid__, None diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/uicfg.py --- a/cubicweb/web/views/uicfg.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/uicfg.py Fri Feb 03 13:37:32 2017 +0100 @@ -54,9 +54,6 @@ uicfg.actionbox_appearsin_addmenu.tag_object_of(('*', 'entry_of', 'Blog'), True) """ - -from warnings import warn - from six import string_types from cubicweb import neg_role @@ -93,7 +90,8 @@ section = 'sideboxes' self.tag_relation((sschema, rschema, oschema, role), section) -primaryview_section = PrimaryViewSectionRelationTags() + +primaryview_section = PrimaryViewSectionRelationTags(__module__=__name__) class DisplayCtrlRelationTags(NoTargetRelationTagsDict): @@ -144,7 +142,7 @@ self.tag_object_of(('*', rtype, etype), {'order': index}) -primaryview_display_ctrl = DisplayCtrlRelationTags() +primaryview_display_ctrl = DisplayCtrlRelationTags(__module__=__name__) # index view configuration #################################################### @@ -155,7 +153,7 @@ # * 'hidden' # * 'subobject' (not displayed by default) -class InitializableDict(dict): # XXX not a rtag. Turn into an appobject? +class InitializableDict(dict): # XXX not a rtag. Turn into an appobject? def __init__(self, *args, **kwargs): super(InitializableDict, self).__init__(*args, **kwargs) self.__defaults = dict(self) @@ -174,12 +172,13 @@ else: self.setdefault(eschema, 'application') + indexview_etype_section = InitializableDict( EmailAddress='subobject', Bookmark='system', # entity types in the 'system' table by default (managers only) CWUser='system', CWGroup='system', - ) +) # autoform.AutomaticEntityForm configuration ################################## @@ -191,6 +190,7 @@ result[formtype] = section return result + def _card_and_comp(sschema, rschema, oschema, role): rdef = rschema.rdef(sschema, oschema) if role == 'subject': @@ -201,6 +201,7 @@ composed = not rschema.final and rdef.composite == 'subject' return card, composed + class AutoformSectionRelationTags(RelationTagsSet): """autoform relations'section""" __regid__ = 'autoform_section' @@ -220,12 +221,7 @@ formsections = self.init_get(sschema, rschema, oschema, role) if formsections is None: formsections = self.tag_container_cls() - if not any(tag.startswith('inlined') for tag in formsections): - if not rschema.final: - negsects = self.init_get(sschema, rschema, oschema, neg_role(role)) - if 'main_inlined' in negsects: - formsections.add('inlined_hidden') - key = _ensure_str_key( (sschema, rschema, oschema, role) ) + key = _ensure_str_key((sschema, rschema, oschema, role)) self._tagdefs[key] = formsections def _initfunc_step2(self, sschema, rschema, oschema, role): @@ -242,31 +238,26 @@ sectdict.setdefault('muledit', 'hidden') sectdict.setdefault('inlined', 'hidden') # ensure we have a tag for each form type - if not 'main' in sectdict: - if not rschema.final and ( - sectdict.get('inlined') == 'attributes' or - 'inlined_attributes' in self.init_get(sschema, rschema, oschema, - neg_role(role))): - sectdict['main'] = 'hidden' - elif sschema.is_metadata(rschema): + if 'main' not in sectdict: + if sschema.is_metadata(rschema): sectdict['main'] = 'metadata' else: card, composed = _card_and_comp(sschema, rschema, oschema, role) if card in '1+': sectdict['main'] = 'attributes' - if not 'muledit' in sectdict: + if 'muledit' not in sectdict: sectdict['muledit'] = 'attributes' elif rschema.final: sectdict['main'] = 'attributes' else: sectdict['main'] = 'relations' - if not 'muledit' in sectdict: + if 'muledit' not in sectdict: sectdict['muledit'] = 'hidden' if sectdict['main'] == 'attributes': card, composed = _card_and_comp(sschema, rschema, oschema, role) if card in '1+' and not composed: sectdict['muledit'] = 'attributes' - if not 'inlined' in sectdict: + if 'inlined' not in sectdict: sectdict['inlined'] = sectdict['main'] # recompute formsections and set it to avoid recomputing for formtype, section in sectdict.items(): @@ -278,11 +269,11 @@ self.tag_relation(key, ftype, section) return assert formtype in self._allowed_form_types, \ - 'formtype should be in (%s), not %s' % ( - ','.join(self._allowed_form_types), formtype) + 'formtype should be in (%s), not %s' % ( + ','.join(self._allowed_form_types), formtype) assert section in self._allowed_values[formtype], \ - 'section for %s should be in (%s), not %s' % ( - formtype, ','.join(self._allowed_values[formtype]), section) + 'section for %s should be in (%s), not %s' % ( + formtype, ','.join(self._allowed_values[formtype]), section) rtags = self._tagdefs.setdefault(_ensure_str_key(key), self.tag_container_cls()) # remove previous section for this form type if any @@ -303,8 +294,8 @@ section, value = tag.split('_', 1) rtags[section] = value cls = self.tag_container_cls - rtags = cls('_'.join([section,value]) - for section,value in rtags.items()) + rtags = cls('_'.join([section, value]) + for section, value in rtags.items()) return rtags def get(self, *key): @@ -320,9 +311,10 @@ bool telling if having local role is enough (strict = False) or not """ tag = '%s_%s' % (formtype, section) - eschema = entity.e_schema + eschema = entity.e_schema cw = entity._cw - permsoverrides = cw.vreg['uicfg'].select('autoform_permissions_overrides', cw, entity=entity) + permsoverrides = cw.vreg['uicfg'].select('autoform_permissions_overrides', cw, + entity=entity) if entity.has_eid(): eid = entity.eid else: @@ -339,7 +331,7 @@ for tschema in targetschemas: # check section's tag first, potentially lower cost than # checking permission which may imply rql queries - if not tag in self.etype_get(eschema, rschema, role, tschema): + if tag not in self.etype_get(eschema, rschema, role, tschema): continue rdef = rschema.role_rdef(eschema, tschema, role) if rschema.final: @@ -361,7 +353,8 @@ # XXX tag allowing to hijack the permission machinery when # permission is not verifiable until the entity is actually # created... - if eid is None and '%s_on_new' % permission in permsoverrides.etype_get(eschema, rschema, role): + if eid is None and '%s_on_new' % permission in permsoverrides.etype_get( + eschema, rschema, role): yield (rschema, targetschemas, role) continue if not rschema.final and role == 'subject': @@ -491,7 +484,8 @@ for attr in attrs: self.edit_as_attr(self, etype, attr, formtype='muledit') -autoform_section = AutoformSectionRelationTags() + +autoform_section = AutoformSectionRelationTags(__module__=__name__) # relations'field class @@ -510,7 +504,8 @@ """ self._tag_etype_attr(etype, attr, '*', field) -autoform_field = AutoformFieldTags() + +autoform_field = AutoformFieldTags(__module__=__name__) # relations'field explicit kwargs (given to field's __init__) @@ -562,7 +557,7 @@ self._tag_etype_attr(etype, attr, '*', kwargs) -autoform_field_kwargs = AutoformFieldKwargsTags() +autoform_field_kwargs = AutoformFieldKwargsTags(__module__=__name__) # set of tags of the form _on_new on relations. is a @@ -571,7 +566,8 @@ class AutoFormPermissionsOverrides(RelationTagsSet): __regid__ = 'autoform_permissions_overrides' -autoform_permissions_overrides = AutoFormPermissionsOverrides() + +autoform_permissions_overrides = AutoFormPermissionsOverrides(__module__=__name__) class ReleditTags(NoTargetRelationTagsDict): @@ -626,13 +622,14 @@ edittarget = 'related' if composite else 'rtype' self.tag_relation((sschema, rschema, oschema, role), {'edit_target': edittarget}) - if not 'novalue_include_rtype' in values: + if 'novalue_include_rtype' not in values: showlabel = primaryview_display_ctrl.get( sschema, rschema, oschema, role).get('showlabel', True) self.tag_relation((sschema, rschema, oschema, role), {'novalue_include_rtype': not showlabel}) -reledit_ctrl = ReleditTags() + +reledit_ctrl = ReleditTags(__module__=__name__) # boxes.EditBox configuration ################################################# @@ -666,7 +663,8 @@ :param etype: the entity type as a string :param attr: the name of the attribute or relation to hide - :param createdtype: the target type of the relation (optional, defaults to '*' (all possible types)) + :param createdtype: the target type of the relation + (optional, defaults to '*' (all possible types)) `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation) @@ -678,14 +676,15 @@ :param etype: the entity type as a string :param attr: the name of the attribute or relation to hide - :param createdtype: the target type of the relation (optional, defaults to '*' (all possible types)) + :param createdtype: the target type of the relation + (optional, defaults to '*' (all possible types)) `attr` can be a string or 2-tuple (relname, role_of_etype_in_the_relation) """ self._tag_etype_attr(etype, attr, createdtype, False) -actionbox_appearsin_addmenu = ActionBoxUicfg() +actionbox_appearsin_addmenu = ActionBoxUicfg(__module__=__name__) def registration_callback(vreg): diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/views/workflow.py --- a/cubicweb/web/views/workflow.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/views/workflow.py Fri Feb 03 13:37:32 2017 +0100 @@ -152,7 +152,8 @@ else: sel += ',WF' headers = (_('from_state'), _('to_state'), _('comment'), _('date')) - rql = '%s %s, X eid %%(x)s' % (sel, rql) + sel += ',FSN,TSN,CF' + rql = '%s %s, FS name FSN, TS name TSN, WF comment_format CF, X eid %%(x)s' % (sel, rql) try: rset = self._cw.execute(rql, {'x': eid}) except Unauthorized: diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/web/webconfig.py --- a/cubicweb/web/webconfig.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/web/webconfig.py Fri Feb 03 13:37:32 2017 +0100 @@ -82,7 +82,7 @@ """the WebConfiguration is a singleton object handling instance's configuration and preferences """ - cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set([join('web', 'views')]) + cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set(['web.views']) cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['views']) options = merge_options(CubicWebConfiguration.options + ( @@ -113,19 +113,6 @@ 'group': 'web', 'level': 3, }), # web configuration - ('https-url', - {'type' : 'string', - 'default': None, - 'help': 'web server root url on https. By specifying this option your '\ - 'site can be available as an http and https site. Authenticated users '\ - 'will in this case be authenticated and once done navigate through the '\ - 'https site. IMPORTANTE NOTE: to do this work, you should have your '\ - 'apache redirection include "https" as base url path so cubicweb can '\ - 'differentiate between http vs https access. For instance: \n'\ - 'RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P]\n'\ - 'where the cubicweb web server is listening on port 8080.', - 'group': 'main', 'level': 3, - }), ('datadir-url', {'type': 'string', 'default': None, 'help': ('base url for static data, if different from "${base-url}/data/". ' @@ -261,9 +248,7 @@ def __init__(self, *args, **kwargs): super(WebConfiguration, self).__init__(*args, **kwargs) self.uiprops = None - self.https_uiprops = None self.datadir_url = None - self.https_datadir_url = None def fckeditor_installed(self): if self.uiprops is None: @@ -382,16 +367,8 @@ self.datadir_url += '/' if self.mode != 'test': self.datadir_url += '%s/' % self.instance_md5_version() - self.https_datadir_url = self.datadir_url return - httpsurl = self['https-url'] data_relpath = self.data_relpath() - if httpsurl: - if httpsurl[-1] != '/': - httpsurl += '/' - if not self.repairing: - self.global_set_option('https-url', httpsurl) - self.https_datadir_url = httpsurl + data_relpath self.datadir_url = baseurl + data_relpath def data_relpath(self): @@ -409,14 +386,6 @@ data=lambda x: self.datadir_url + x, datadir_url=self.datadir_url[:-1]) self._init_uiprops(self.uiprops) - if self['https-url']: - cachedir = join(self.appdatahome, 'uicachehttps') - self.check_writeable_uid_directory(cachedir) - self.https_uiprops = PropertySheet( - cachedir, - data=lambda x: self.https_datadir_url + x, - datadir_url=self.https_datadir_url[:-1]) - self._init_uiprops(self.https_uiprops) def _init_uiprops(self, uiprops): libuiprops = join(self.shared_dir(), 'data', 'uiprops.py') diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/wsgi/request.py --- a/cubicweb/wsgi/request.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/wsgi/request.py Fri Feb 03 13:37:32 2017 +0100 @@ -69,15 +69,10 @@ 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, + super(CubicWebWsgiRequest, self).__init__(vreg, post, headers= headers_in) self.content = environ['wsgi.input'] if files is not None: @@ -121,9 +116,6 @@ ## wsgi request helpers ################################################### - def is_secure(self): - return self.environ['wsgi.url_scheme'] == 'https' - def get_posted_data(self): # The WSGI spec says 'QUERY_STRING' may be absent. post = parse_qs(self.environ.get('QUERY_STRING', '')) diff -r f96f77a190f2 -r 3c58ea2fd745 cubicweb/wsgi/test/unittest_wsgi.py --- a/cubicweb/wsgi/test/unittest_wsgi.py Tue Jan 31 11:06:28 2017 +0100 +++ b/cubicweb/wsgi/test/unittest_wsgi.py Fri Feb 03 13:37:32 2017 +0100 @@ -27,30 +27,6 @@ self.assertEqual(b'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 = b'x'*100001 r = webtest.app.TestRequest.blank('/', { diff -r f96f77a190f2 -r 3c58ea2fd745 debian/control --- a/debian/control Tue Jan 31 11:06:28 2017 +0100 +++ b/debian/control Fri Feb 03 13:37:32 2017 +0100 @@ -25,7 +25,7 @@ python-pyramid, python-pyramid-multiauth, python-waitress, - python-passlib (<< 2.0), + python-passlib (>= 1.7.0), python-wsgicors, sphinx-common, Standards-Version: 3.9.6 diff -r f96f77a190f2 -r 3c58ea2fd745 doc/api/pyramid/authplugin.rst --- a/doc/api/pyramid/authplugin.rst Tue Jan 31 11:06:28 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,10 +0,0 @@ -.. _authplugin_module: - -:mod:`cubicweb.pyramid.authplugin` ----------------------------------- - -.. automodule:: cubicweb.pyramid.authplugin - - .. autoclass:: DirectAuthentifier - :show-inheritance: - :members: diff -r f96f77a190f2 -r 3c58ea2fd745 doc/api/pyramid/tools.rst --- a/doc/api/pyramid/tools.rst Tue Jan 31 11:06:28 2017 +0100 +++ b/doc/api/pyramid/tools.rst Fri Feb 03 13:37:32 2017 +0100 @@ -1,7 +1,7 @@ .. _tools_module: :mod:`cubicweb.pyramid.tools` ----------------------------- +----------------------------- .. automodule:: cubicweb.pyramid.tools diff -r f96f77a190f2 -r 3c58ea2fd745 doc/book/admin/instance-config.rst --- a/doc/book/admin/instance-config.rst Tue Jan 31 11:06:28 2017 +0100 +++ b/doc/book/admin/instance-config.rst Fri Feb 03 13:37:32 2017 +0100 @@ -42,12 +42,9 @@ :`main.base-url`: url base site to be used to generate the urls of web pages -Https configuration -``````````````````` -It is possible to make a site accessible for anonymous http connections -and https for authenticated users. This requires to -use apache (for example) for redirection and the variable `main.https-url` -of configuration file. +Apache configuration +```````````````````` +It is possible to use apache (for example) as proxy. For this to work you have to activate the following apache modules : @@ -62,9 +59,8 @@ :Example: - For an apache redirection of a site accessible via `http://localhost/demo` - and `https://localhost/demo` and actually running on port 8080, it - takes to the http::: + For an apache redirection of a site accessible via `http://localhost/demo` while cubicweb is + actually running on port 8080::: ProxyPreserveHost On RewriteEngine On @@ -72,24 +68,11 @@ RewriteRule ^/demo$ /demo/ RewriteRule ^/demo/(.*) http://127.0.0.1:8080/$1 [L,P] - and for the https::: - - ProxyPreserveHost On - RewriteEngine On - RewriteCond %{REQUEST_URI} ^/ demo - RewriteRule ^/demo$/demo/ - RewriteRule ^/demo/(.*) http://127.0.0.1:8080/https/$1 [L,P] - and we will file in the all-in-one.conf of the instance::: base-url = http://localhost/demo - https-url = https://localhost/demo -Notice that if you simply want a site accessible through https, not *both* http -and https, simply set `base-url` to the https url and the first section into your -apache configuration (as you would have to do for an http configuration with an -apache front-end). Setting up the web client ------------------------- diff -r f96f77a190f2 -r 3c58ea2fd745 doc/book/devweb/edition/dissection.rst --- a/doc/book/devweb/edition/dissection.rst Tue Jan 31 11:06:28 2017 +0100 +++ b/doc/book/devweb/edition/dissection.rst Fri Feb 03 13:37:32 2017 +0100 @@ -107,14 +107,14 @@ + type="text" value="let us write more doc" /> ... (description field omitted) ... - @@ -126,7 +126,7 @@ - @@ -134,7 +134,7 @@ - @@ -180,7 +180,7 @@    -