--- a/MANIFEST.in Mon Mar 20 09:40:24 2017 +0100
+++ b/MANIFEST.in Mon Mar 20 10:28:01 2017 +0100
@@ -62,6 +62,8 @@
recursive-include cubicweb/web/test/data bootstrap_cubes pouet.css *.py
recursive-include cubicweb/web/test/data/static/jstests *.js *.html *.json
+include cubicweb/pyramid/development.ini.tmpl
+
include cubicweb/web/data/jquery-treeview/*.md
recursive-include cubicweb/skeleton *.py *.css *.js *.po compat *.tmpl rules
--- a/cubicweb.spec Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb.spec Mon Mar 20 10:28:01 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
--- a/cubicweb/__init__.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/__init__.py Mon Mar 20 10:28:01 2017 +0100
@@ -24,8 +24,8 @@
import logging
import os
import pickle
-import pkgutil
import sys
+import types
import warnings
import zlib
@@ -282,6 +282,37 @@
# Import hook for "legacy" cubes ##############################################
+class _CubesLoader(object):
+
+ def __init__(self, *modinfo):
+ self.modinfo = modinfo
+
+ def load_module(self, fullname):
+ try:
+ # If there is an existing module object named 'fullname' in
+ # sys.modules , the loader must use that existing module.
+ # Otherwise, the reload() builtin will not work correctly.
+ return sys.modules[fullname]
+ except KeyError:
+ pass
+ if fullname == 'cubes':
+ mod = sys.modules[fullname] = types.ModuleType(
+ fullname, doc='CubicWeb cubes')
+ else:
+ modname, file, pathname, description = self.modinfo
+ try:
+ mod = sys.modules[fullname] = imp.load_module(
+ modname, file, pathname, description)
+ finally:
+ # https://docs.python.org/2/library/imp.html#imp.load_module
+ # Important: the caller is responsible for closing the file
+ # argument, if it was not None, even when an exception is
+ # raised. This is best done using a try ... finally statement
+ if file is not None:
+ file.close()
+ return mod
+
+
class _CubesImporter(object):
"""Module finder handling redirection of import of "cubes.<name>"
to "cubicweb_<name>".
@@ -294,11 +325,13 @@
sys.meta_path.append(self)
def find_module(self, fullname, path=None):
- if fullname.startswith('cubes.'):
+ if fullname == 'cubes':
+ return _CubesLoader()
+ elif fullname.startswith('cubes.') and fullname.count('.') == 1:
modname = 'cubicweb_' + fullname.split('.', 1)[1]
try:
modinfo = imp.find_module(modname)
except ImportError:
return None
else:
- return pkgutil.ImpLoader(fullname, *modinfo)
+ return _CubesLoader(modname, *modinfo)
--- a/cubicweb/__pkginfo__.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/__pkginfo__.py Mon Mar 20 10:28:01 2017 +0100
@@ -27,8 +27,8 @@
modname = distname = "cubicweb"
-numversion = (3, 24, 6)
-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"
--- a/cubicweb/cwconfig.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/cwconfig.py Mon Mar 20 10:28:01 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,13 +220,15 @@
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
"""
- return [name for name in ('repository', 'all-in-one')
+ return [name for name in ('repository', 'all-in-one', 'pyramid')
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',
@@ -467,6 +503,7 @@
@classmethod
def available_cubes(cls):
cubes = set()
+ prefix = 'cubicweb_'
for entry_point in pkg_resources.iter_entry_points(
group='cubicweb.cubes', name=None):
try:
@@ -475,11 +512,11 @@
continue
else:
modname = module.__name__
- if not modname.startswith('cubicweb_'):
+ if not modname.startswith(prefix):
cls.warning('entry point %s does not appear to be a cube',
entry_point)
continue
- cubes.add(modname)
+ cubes.add(modname[len(prefix):])
# Legacy cubes.
for directory in cls.cubes_search_path():
if not exists(directory):
@@ -667,23 +704,18 @@
"""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):
for confmod in ('web.webconfig', 'etwist.twconfig',
- 'server.serverconfig',):
+ 'server.serverconfig', 'pyramid.config'):
try:
__import__('cubicweb.%s' % confmod)
- except ImportError:
- pass
+ except ImportError as exc:
+ cls.warning('failed to load config module %s (%s)',
+ confmod, exc)
@classmethod
def load_cwctl_plugins(cls):
@@ -692,7 +724,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 +812,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=()):
@@ -1116,9 +1119,11 @@
# config -> repository
def repository(self, vreg=None):
+ """Return a new bootstrapped repository."""
from cubicweb.server.repository import Repository
- from cubicweb.server.utils import TasksManager
- return Repository(self, TasksManager(), vreg=vreg)
+ repo = Repository(self, vreg=vreg)
+ repo.bootstrap()
+ return repo
# instance methods used to get instance specific resources #############
@@ -1309,21 +1314,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:
--- a/cubicweb/cwctl.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/cwctl.py Mon Mar 20 10:28:01 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 <command>_method on each argument (a list of instance
identifiers)
@@ -332,7 +326,7 @@
}),
('config',
{'short': 'c', 'type' : 'choice', 'metavar': '<install type>',
- 'choices': ('all-in-one', 'repository'),
+ 'choices': ('all-in-one', 'repository', 'pyramid'),
'default': 'all-in-one',
'help': 'installation type, telling which part of an instance '
'should be installed. You can list available configurations using the'
@@ -519,8 +513,8 @@
msg = (
"Twisted is required by the 'start' command\n"
"Either install it, or use one of the alternative commands:\n"
- "- '{ctl} wsgi {appid}'\n"
- "- '{ctl} pyramid {appid}' (requires the pyramid cube)\n")
+ "- '{ctl} pyramid {appid}'\n"
+ "- '{ctl} wsgi {appid}'\n")
raise ExecutionError(msg.format(ctl='cubicweb-ctl', appid=appid))
config = cwcfg.config_for(appid, debugmode=self['debug'])
# override config file values with cmdline options
@@ -757,6 +751,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
--- a/cubicweb/cwvreg.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/cwvreg.py Mon Mar 20 10:28:01 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):
--- a/cubicweb/dataimport/importer.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/dataimport/importer.py Mon Mar 20 10:28:01 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.
--- a/cubicweb/dataimport/massive_store.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/dataimport/massive_store.py Mon Mar 20 10:28:01 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
--- a/cubicweb/dataimport/stores.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/dataimport/stores.py Mon Mar 20 10:28:01 2017 +0100
@@ -149,11 +149,6 @@
def commit(self):
return self._commit()
- @property
- def session(self):
- warnings.warn('[3.19] deprecated property.', DeprecationWarning, stacklevel=2)
- return self._cnx.repo._get_session(self._cnx.sessionid)
-
@deprecated("[3.19] use cnx.find(*args, **kwargs).entities() instead")
def find_entities(self, *args, **kwargs):
return self._cnx.find(*args, **kwargs).entities()
--- a/cubicweb/dataimport/test/test_massive_store.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/dataimport/test/test_massive_store.py Mon Mar 20 10:28:01 2017 +0100
@@ -16,8 +16,6 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Massive store test case"""
-import itertools
-
from cubicweb.devtools import testlib, PostgresApptestConfiguration
from cubicweb.devtools import startpgcluster, stoppgcluster
from cubicweb.dataimport import ucsvreader, stores
--- a/cubicweb/devtools/__init__.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/__init__.py Mon Mar 20 10:28:01 2017 +0100
@@ -108,7 +108,6 @@
* system source is shutdown
"""
if not repo._needs_refresh:
- repo.close_sessions()
for cnxset in repo.cnxsets:
cnxset.close(True)
repo.system_source.shutdown()
@@ -125,9 +124,7 @@
if repo._needs_refresh:
for cnxset in repo.cnxsets:
cnxset.reconnect()
- repo._type_cache = {}
- repo.querier._rql_cache = {}
- repo.system_source.reset_caches()
+ repo.clear_caches()
repo._needs_refresh = False
--- a/cubicweb/devtools/devctl.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/devctl.py Mon Mar 20 10:28:01 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']
--- a/cubicweb/devtools/fake.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/fake.py Mon Mar 20 10:28:01 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:
@@ -135,7 +132,7 @@
return True
-class FakeSession(RequestSessionBase):
+class FakeConnection(RequestSessionBase):
def __init__(self, repo=None, user=None, vreg=None):
self.repo = repo
@@ -154,8 +151,7 @@
def commit(self, *args):
self.transaction_data.clear()
- def close(self, *args):
- pass
+
def system_sql(self, sql, args=None):
pass
@@ -187,9 +183,6 @@
self.vreg = vreg or FakeCWRegistryStore(self.config, initlog=False)
self.vreg.schema = schema
- def internal_session(self):
- return FakeSession(self)
-
class FakeSource(object):
dbhelper = get_db_helper('sqlite')
--- a/cubicweb/devtools/repotest.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/repotest.py Mon Mar 20 10:28:01 2017 +0100
@@ -21,13 +21,13 @@
"""
from __future__ import print_function
+from contextlib import contextmanager
from pprint import pprint
-from logilab.common.decorators import cachedproperty
from logilab.common.testlib import SkipTest
from cubicweb.devtools.testlib import RepoAccess
-
+from cubicweb.entities.authobjs import user_session_cache_key
def tuplify(mylist):
return [tuple(item) for item in mylist]
@@ -39,7 +39,7 @@
def check_plan(self, rql, expected, kwargs=None):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
plan = self._prepare_plan(cnx, rql, kwargs)
self.planner.build_plan(plan)
try:
@@ -135,10 +135,9 @@
from rql import RQLHelper
from cubicweb.devtools.testlib import BaseTestCase
-from cubicweb.devtools.fake import FakeRepo, FakeConfig, FakeSession, FakeRequest
+from cubicweb.devtools.fake import FakeRepo, FakeConfig, FakeRequest, FakeConnection
from cubicweb.server import set_debug, debugged
from cubicweb.server.querier import QuerierHelper
-from cubicweb.server.session import Session
from cubicweb.server.sources.rql2sql import SQLGenerator, remove_unused_solutions
class RQLGeneratorTC(BaseTestCase):
@@ -182,7 +181,7 @@
#print '********* solutions', solutions
self.rqlhelper.simplify(union)
#print '********* simplified', union.as_string()
- plan = self.qhelper.plan_factory(union, {}, FakeSession(self.repo))
+ plan = self.qhelper.plan_factory(union, {}, FakeConnection(self.repo))
plan.preprocess(union)
for select in union.children:
select.solutions.sort(key=lambda x: list(x.items()))
@@ -193,14 +192,10 @@
class BaseQuerierTC(TestCase):
repo = None # set this in concrete class
- @cachedproperty
- def session(self):
- return self._access._session
-
def setUp(self):
self.o = self.repo.querier
- self._access = RepoAccess(self.repo, 'admin', FakeRequest)
- self.ueid = self.session.user.eid
+ self.admin_access = RepoAccess(self.repo, 'admin', FakeRequest)
+ self.ueid = self.admin_access._user.eid
assert self.ueid != -1
self.repo._type_cache = {} # clear cache
self.maxeid = self.get_max_eid()
@@ -208,18 +203,18 @@
self._dumb_sessions = []
def get_max_eid(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
return cnx.execute('Any MAX(X)')[0][0]
def cleanup(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
cnx.execute('DELETE Any X WHERE X eid > %s' % self.maxeid)
cnx.commit()
def tearDown(self):
undo_monkey_patch()
self.cleanup()
- assert self.session.user.eid != -1
+ assert self.admin_access._user.eid != -1
def set_debug(self, debug):
set_debug(debug)
@@ -250,17 +245,17 @@
rqlst.solutions = remove_unused_solutions(rqlst, rqlst.solutions, self.repo.schema)[0]
return rqlst
+ @contextmanager
def user_groups_session(self, *groups):
"""lightweight session using the current user with hi-jacked groups"""
- # use self.session.user.eid to get correct owned_by relation, unless explicit eid
- with self.session.new_cnx() as cnx:
- user_eid = self.session.user.eid
- session = Session(self.repo._build_user(cnx, user_eid), self.repo)
- session.data['%s-groups' % user_eid] = set(groups)
- return session
+ # use cnx.user.eid to get correct owned_by relation, unless explicit eid
+ with self.admin_access.cnx() as cnx:
+ user_eid = cnx.user.eid
+ cnx.user._cw.data[user_session_cache_key(user_eid, 'groups')] = set(groups)
+ yield cnx
def qexecute(self, rql, args=None, build_descr=True):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
try:
return self.o.execute(cnx, rql, args, build_descr)
finally:
@@ -273,7 +268,6 @@
def setup(self):
# XXX source_defs
self.o = self.repo.querier
- self.session = self.repo._sessions.values()[0]
self.schema = self.o.schema
self.system = self.repo.system_source
do_monkey_patch()
@@ -283,8 +277,8 @@
undo_monkey_patch()
def _prepare_plan(self, cnx, rql, kwargs=None):
- rqlst = self.o.parse(rql, annotate=True)
- self.o.solutions(cnx, rqlst, kwargs)
+ rqlst = self.repo.vreg.rqlhelper.parse(rql, annotate=True)
+ self.repo.vreg.solutions(cnx, rqlst, kwargs)
if rqlst.TYPE == 'select':
self.repo.vreg.rqlhelper.annotate(rqlst)
for select in rqlst.children:
--- a/cubicweb/devtools/stresstester.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/stresstester.py Mon Mar 20 10:28:01 2017 +0100
@@ -54,19 +54,23 @@
from logilab.common.fileutils import lines
from logilab.common.ureports import Table, TextWriter
+
+from cubicweb import repoapi
from cubicweb.server.repository import Repository
TB_LOCK = threading.Lock()
+
class QueryExecutor:
- def __init__(self, session, times, queries, reporter = None):
- self._session = session
+
+ def __init__(self, cnx, times, queries, reporter = None):
+ self._cnx = cnx
self._times = times
self._queries = queries
self._reporter = reporter
def run(self):
- with self._session.new_cnx() as cnx:
+ with self._cnx as cnx:
times = self._times
while times:
for index, query in enumerate(self._queries):
@@ -168,12 +172,13 @@
# get local access to the repository
print("Creating repo", prof_file)
repo = Repository(config, prof_file)
- session = repo.new_session(user, password=password)
+ repo.bootstrap()
+ cnx = repoapi.connect(repo, user, password=password)
reporter = ProfileReporter(queries)
if threads > 1:
executors = []
while threads:
- qe = QueryExecutor(session, repeat, queries, reporter = reporter)
+ qe = QueryExecutor(cnx, repeat, queries, reporter=reporter)
executors.append(qe)
thread = threading.Thread(target=qe.run)
qe.thread = thread
@@ -184,7 +189,7 @@
## for qe in executors:
## print qe.thread, repeat - qe._times, 'times'
else:
- QueryExecutor(session, repeat, queries, reporter = reporter).run()
+ QueryExecutor(cnx, repeat, queries, reporter=reporter).run()
reporter.dump_report(report_output)
--- a/cubicweb/devtools/test/unittest_devctl.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/test/unittest_devctl.py Mon Mar 20 10:28:01 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]
@@ -51,8 +51,7 @@
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__':
--- a/cubicweb/devtools/test/unittest_testlib.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/test/unittest_testlib.py Mon Mar 20 10:28:01 2017 +0100
@@ -285,10 +285,6 @@
self.assertTrue(rset)
self.assertEqual('babar', req.form['elephant'])
- def test_close(self):
- acc = self.new_access('admin')
- acc.close()
-
def test_admin_access(self):
with self.admin_access.client_cnx() as cnx:
self.assertEqual('admin', cnx.user.login)
--- a/cubicweb/devtools/testlib.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/devtools/testlib.py Mon Mar 20 10:28:01 2017 +0100
@@ -44,16 +44,15 @@
from cubicweb import (ValidationError, NoSelectableObject, AuthenticationError,
BadConnectionId)
-from cubicweb import cwconfig, devtools, web, server
+from cubicweb import cwconfig, devtools, repoapi, server, web
from cubicweb.utils import json
from cubicweb.sobjects import notification
from cubicweb.web import Redirect, application, eid_param
from cubicweb.server.hook import SendMailOp
-from cubicweb.server.session import Session
from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
from cubicweb.devtools import fake, htmlparser, DEFAULT_EMPTY_DB_ID
from cubicweb.devtools.fill import insert_entity_queries, make_relations_queries
-
+from cubicweb.web.views.authentication import Session
if sys.version_info[:2] < (3, 4):
from unittest2 import TestCase
@@ -223,42 +222,20 @@
.. automethod:: cubicweb.testlib.RepoAccess.cnx
.. automethod:: cubicweb.testlib.RepoAccess.web_request
-
- The RepoAccess need to be closed to destroy the associated Session.
- TestCase usually take care of this aspect for the user.
-
- .. automethod:: cubicweb.testlib.RepoAccess.close
"""
def __init__(self, repo, login, requestcls):
self._repo = repo
self._login = login
self.requestcls = requestcls
- self._session = self._unsafe_connect(login)
-
- def _unsafe_connect(self, login, **kwargs):
- """ a completely unsafe connect method for the tests """
- # use an internal connection
- with self._repo.internal_cnx() as cnx:
- # try to get a user object
- user = cnx.find('CWUser', login=login).one()
- user.groups
- user.properties
- user.login
- session = Session(user, self._repo)
- self._repo._sessions[session.sessionid] = session
- user._cw = user.cw_rset.req = session
- with session.new_cnx() as cnx:
- self._repo.hm.call_hooks('session_open', cnx)
- # commit connection at this point in case write operation has been
- # done during `session_open` hooks
- cnx.commit()
- return session
+ with repo.internal_cnx() as cnx:
+ self._user = cnx.find('CWUser', login=login).one()
+ self._user.cw_attr_cache['login'] = login
@contextmanager
def cnx(self):
"""Context manager returning a server side connection for the user"""
- with self._session.new_cnx() as cnx:
+ with repoapi.Connection(self._repo, self._user) as cnx:
yield cnx
# aliases for bw compat
@@ -273,20 +250,19 @@
req.cnx.commit()
req.cnx.rolback()
"""
+ session = kwargs.pop('session', Session(self._repo, self._user))
req = self.requestcls(self._repo.vreg, url=url, headers=headers,
method=method, form=kwargs)
- with self._session.new_cnx() as cnx:
+ with self.cnx() as cnx:
+ # web request expect a session attribute on cnx referencing the web session
+ cnx.session = session
req.set_cnx(cnx)
yield req
- def close(self):
- """Close the session associated to the RepoAccess"""
- self._session.close()
-
@contextmanager
def shell(self):
from cubicweb.server.migractions import ServerMigrationHelper
- with self._session.new_cnx() as cnx:
+ with self.cnx() as cnx:
mih = ServerMigrationHelper(None, repo=self._repo, cnx=cnx,
interactive=False,
# hack so it don't try to load fs schema
@@ -333,7 +309,6 @@
cls.config.mode = 'test'
def __init__(self, *args, **kwargs):
- self._admin_session = None
self.repo = None
self._open_access = set()
super(CubicWebTC, self).__init__(*args, **kwargs)
@@ -360,15 +335,10 @@
def _close_access(self):
while self._open_access:
try:
- self._open_access.pop().close()
+ self._open_access.pop()
except BadConnectionId:
continue # already closed
- @property
- def session(self):
- """return admin session"""
- return self._admin_session
-
def _init_repo(self):
"""init the repository and connection to it.
"""
@@ -380,7 +350,6 @@
# get an admin session (without actual login)
login = text_type(db_handler.config.default_admin_config['login'])
self.admin_access = self.new_access(login)
- self._admin_session = self.admin_access._session
# config management ########################################################
@@ -456,10 +425,6 @@
MAILBOX[:] = [] # reset mailbox
def tearDown(self):
- # XXX hack until logilab.common.testlib is fixed
- if self._admin_session is not None:
- self._admin_session.close()
- self._admin_session = None
while self._cleanups:
cleanup, args, kwargs = self._cleanups.pop(-1)
cleanup(*args, **kwargs)
@@ -679,7 +644,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):
@@ -715,10 +681,10 @@
return ctrl.publish(), req
@contextmanager
- def remote_calling(self, fname, *args):
+ def remote_calling(self, fname, *args, **kwargs):
"""remote json call simulation"""
args = [json.dumps(arg) for arg in args]
- with self.admin_access.web_request(fname=fname, pageid='123', arg=args) as req:
+ with self.admin_access.web_request(fname=fname, pageid='123', arg=args, **kwargs) as req:
ctrl = self.vreg['controllers'].select('ajax', req)
yield ctrl.publish(), req
@@ -900,15 +866,15 @@
authm.anoninfo = authm.anoninfo[0], {'password': authm.anoninfo[1]}
# not properly cleaned between tests
self.open_sessions = sh.session_manager._sessions = {}
- return req, self.session
+ return req
- def assertAuthSuccess(self, req, origsession, nbsessions=1):
+ def assertAuthSuccess(self, req, nbsessions=1):
session = self.app.get_session(req)
cnx = session.new_cnx()
with cnx:
req.set_cnx(cnx)
self.assertEqual(len(self.open_sessions), nbsessions, self.open_sessions)
- self.assertEqual(session.login, origsession.login)
+ self.assertEqual(req.user.login, self.admlogin)
self.assertEqual(session.anonymous_session, False)
def assertAuthFailure(self, req, nbsessions=0):
--- a/cubicweb/entities/__init__.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/entities/__init__.py Mon Mar 20 10:28:01 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):
--- a/cubicweb/entities/adapters.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/entities/adapters.py Mon Mar 20 10:28:01 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')
--- a/cubicweb/entities/authobjs.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/entities/authobjs.py Mon Mar 20 10:28:01 2017 +0100
@@ -17,8 +17,6 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""entity classes user and group entities"""
-
-
from six import string_types, text_type
from logilab.common.decorators import cached
@@ -173,14 +171,3 @@
return self.login
dc_long_title = name
-
- def __call__(self, *args, **kwargs):
- """ugly hack for compatibility betweeb dbapi and repo api
-
- In the dbapi, Connection and Session have a ``user`` method to
- generated a user for a request In the repo api, Connection and Session
- have a user attribute inherited from SessionRequestBase prototype. This
- ugly hack allows to not break user of the user method.
-
- XXX Deprecate me ASAP"""
- return self
--- a/cubicweb/entity.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/entity.py Mon Mar 20 10:28:01 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]
--- a/cubicweb/etwist/http.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/http.py Mon Mar 20 10:28:01 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)
--- a/cubicweb/etwist/request.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/request.py Mon Mar 20 10:28:01 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 <http://www.gnu.org/licenses/>.
"""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[<filefield>]
"""
- 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
--- a/cubicweb/etwist/server.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/server.py Mon Mar 20 10:28:01 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,12 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""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
+import warnings
from cubicweb.statsd_logger import statsd_timeit
@@ -44,6 +42,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 +58,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']
@@ -71,7 +69,10 @@
if config.mode != 'test':
reactor.addSystemEventTrigger('before', 'shutdown',
self.shutdown_event)
- self.appli.repo.start_looping_tasks()
+ warnings.warn(
+ 'twisted server does not start repository looping tasks anymore; '
+ 'use the standalone "scheduler" command if needed'
+ )
self.set_url_rewriter()
CW_EVENT_MANAGER.bind('after-registry-reload', self.set_url_rewriter)
@@ -92,13 +93,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 +131,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)
--- a/cubicweb/etwist/service.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/service.py Mon Mar 20 10:28:01 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
--- a/cubicweb/etwist/twconfig.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/twconfig.py Mon Mar 20 10:28:01 2017 +0100
@@ -28,6 +28,7 @@
from logilab.common.configuration import Method, merge_options
from cubicweb.cwconfig import CONFIGURATIONS
+from cubicweb.server.serverconfig import ServerConfiguration
from cubicweb.web.webconfig import WebConfiguration
@@ -96,20 +97,14 @@
return 'http://%s:%s/' % (self['host'] or getfqdn().lower(), self['port'] or 8080)
-try:
- from cubicweb.server.serverconfig import ServerConfiguration
+class AllInOneConfiguration(WebConfigurationBase, ServerConfiguration):
+ """repository and web instance in the same twisted process"""
+ name = 'all-in-one'
+ options = merge_options(WebConfigurationBase.options
+ + ServerConfiguration.options)
- class AllInOneConfiguration(WebConfigurationBase, ServerConfiguration):
- """repository and web instance in the same twisted process"""
- name = 'all-in-one'
- options = merge_options(WebConfigurationBase.options
- + ServerConfiguration.options)
-
- cubicweb_appobject_path = WebConfigurationBase.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
- cube_appobject_path = WebConfigurationBase.cube_appobject_path | ServerConfiguration.cube_appobject_path
+ cubicweb_appobject_path = WebConfigurationBase.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
+ cube_appobject_path = WebConfigurationBase.cube_appobject_path | ServerConfiguration.cube_appobject_path
- CONFIGURATIONS.append(AllInOneConfiguration)
-
-except ImportError:
- pass
+CONFIGURATIONS.append(AllInOneConfiguration)
--- a/cubicweb/etwist/twctl.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/etwist/twctl.py Mon Mar 20 10:28:01 2017 +0100
@@ -19,6 +19,7 @@
from cubicweb.toolsutils import CommandHandler
from cubicweb.web.webctl import WebCreateHandler, WebUpgradeHandler
+from cubicweb.server import serverctl
# trigger configuration registration
import cubicweb.etwist.twconfig # pylint: disable=W0611
@@ -45,35 +46,30 @@
cfgname = 'twisted'
-try:
- from cubicweb.server import serverctl
- class AllInOneCreateHandler(serverctl.RepositoryCreateHandler,
- TWCreateHandler):
- """configuration to get an instance running in a twisted web server
- integrating a repository server in the same process
- """
- cfgname = 'all-in-one'
+class AllInOneCreateHandler(serverctl.RepositoryCreateHandler,
+ TWCreateHandler):
+ """configuration to get an instance running in a twisted web server
+ integrating a repository server in the same process
+ """
+ cfgname = 'all-in-one'
- def bootstrap(self, cubes, automatic=False, inputlevel=0):
- """bootstrap this configuration"""
- serverctl.RepositoryCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
- TWCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
+ def bootstrap(self, cubes, automatic=False, inputlevel=0):
+ """bootstrap this configuration"""
+ serverctl.RepositoryCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
+ TWCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
- class AllInOneStartHandler(TWStartHandler):
- cmdname = 'start'
- cfgname = 'all-in-one'
- subcommand = 'cubicweb-twisted'
+class AllInOneStartHandler(TWStartHandler):
+ cmdname = 'start'
+ cfgname = 'all-in-one'
+ subcommand = 'cubicweb-twisted'
- class AllInOneStopHandler(CommandHandler):
- cmdname = 'stop'
- cfgname = 'all-in-one'
- subcommand = 'cubicweb-twisted'
+class AllInOneStopHandler(CommandHandler):
+ cmdname = 'stop'
+ cfgname = 'all-in-one'
+ subcommand = 'cubicweb-twisted'
- def poststop(self):
- pass
+ def poststop(self):
+ pass
- class AllInOneUpgradeHandler(TWUpgradeHandler):
- cfgname = 'all-in-one'
-
-except ImportError:
- pass
+class AllInOneUpgradeHandler(TWUpgradeHandler):
+ cfgname = 'all-in-one'
--- a/cubicweb/hooks/__init__.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/hooks/__init__.py Mon Mar 20 10:28:01 2017 +0100
@@ -29,6 +29,8 @@
events = ('server_startup',)
def __call__(self):
+ if self.repo._scheduler is None:
+ return
# XXX use named args and inner functions to avoid referencing globals
# which may cause reloading pb
lifetime = timedelta(days=self.repo.config['keep-transaction-lifetime'])
@@ -49,6 +51,8 @@
events = ('server_startup',)
def __call__(self):
+ if self.repo._scheduler is None:
+ return
def update_feeds(repo):
# take a list to avoid iterating on a dictionary whose size may
# change
@@ -71,6 +75,8 @@
events = ('server_startup',)
def __call__(self):
+ if self.repo._scheduler is None:
+ return
def expire_dataimports(repo=self.repo):
for uri, source in repo.sources_by_uri.items():
if (uri == 'system'
--- a/cubicweb/hooks/syncschema.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/hooks/syncschema.py Mon Mar 20 10:28:01 2017 +0100
@@ -217,10 +217,6 @@
repo.schema.rebuild_infered_relations()
# trigger vreg reload
repo.set_schema(repo.schema)
- # CWUser class might have changed, update current session users
- cwuser_cls = self.cnx.vreg['etypes'].etype_class('CWUser')
- for session in repo._sessions.values():
- session.user.__class__ = cwuser_cls
except Exception:
self.critical('error while setting schema', exc_info=True)
--- a/cubicweb/hooks/syncsession.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/hooks/syncsession.py Mon Mar 20 10:28:01 2017 +0100
@@ -24,11 +24,9 @@
from cubicweb.entities.authobjs import user_session_cache_key
-# take cnx and not repo because it's needed for other sessions implementation (e.g. pyramid)
-def get_user_sessions(cnx, ueid):
- for session in cnx.repo._sessions.values():
- if ueid == session.user.eid:
- yield session
+def get_user_sessions(cnx, user_eid):
+ if cnx.user.eid == user_eid:
+ yield cnx
class CachedValueMixin(object):
@@ -118,7 +116,6 @@
# remove cached groups for the user
key = user_session_cache_key(self.session.user.eid, 'groups')
self.session.data.pop(key, None)
- self.session.repo.close(self.session.sessionid)
except BadConnectionId:
pass # already closed
--- a/cubicweb/hooks/test/unittest_security.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/hooks/test/unittest_security.py Mon Mar 20 10:28:01 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()
--- a/cubicweb/i18n/de.po Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/i18n/de.po Mon Mar 20 10:28:01 2017 +0100
@@ -198,6 +198,9 @@
msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
msgstr ""
+msgid "And more composite entities"
+msgstr ""
+
msgid "Attributes permissions:"
msgstr "Rechte der Attribute"
--- a/cubicweb/i18n/en.po Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/i18n/en.po Mon Mar 20 10:28:01 2017 +0100
@@ -187,6 +187,9 @@
msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
msgstr ""
+msgid "And more composite entities"
+msgstr ""
+
msgid "Attributes permissions:"
msgstr ""
--- a/cubicweb/i18n/es.po Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/i18n/es.po Mon Mar 20 10:28:01 2017 +0100
@@ -201,6 +201,9 @@
msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
msgstr "Relación agregada : %(entity_from)s %(rtype)s %(entity_to)s"
+msgid "And more composite entities"
+msgstr ""
+
msgid "Attributes permissions:"
msgstr "Permisos de atributos:"
--- a/cubicweb/i18n/fr.po Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/i18n/fr.po Mon Mar 20 10:28:01 2017 +0100
@@ -195,6 +195,9 @@
msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
msgstr "Relation ajoutée : %(entity_from)s %(rtype)s %(entity_to)s"
+msgid "And more composite entities"
+msgstr "Et d'autres entités"
+
msgid "Attributes permissions:"
msgstr "Permissions des attributs"
--- a/cubicweb/predicates.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/predicates.py Mon Mar 20 10:28:01 2017 +0100
@@ -868,7 +868,7 @@
class partial_has_related_entities(PartialPredicateMixIn, has_related_entities):
- """Same as :class:~`cubicweb.predicates.has_related_entity`, but will look
+ """Same as :class:~`cubicweb.predicates.has_related_entities`, but will look
for attributes of the selected class to get information which is otherwise
expected by the initializer.
--- a/cubicweb/pyramid/__init__.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/__init__.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,9 +1,34 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
+"""Pyramid interface to CubicWeb"""
+
+import atexit
import os
from warnings import warn
+
import wsgicors
from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
from pyramid.config import Configurator
+from pyramid.exceptions import ConfigurationError
from pyramid.settings import asbool, aslist
try:
@@ -12,11 +37,9 @@
from ConfigParser import SafeConfigParser
-def make_cubicweb_application(cwconfig, settings=None):
- """
- Create a pyramid-based CubicWeb instance from a cubicweb configuration.
-
- It is initialy meant to be used by the 'pyramid' command of cubicweb-ctl.
+def config_from_cwconfig(cwconfig, settings=None):
+ """Return a Pyramid Configurator instance built from a CubicWeb config and
+ Pyramid-specific configuration files (pyramid.ini).
:param cwconfig: A CubicWeb configuration
:returns: A Pyramid config object
@@ -74,7 +97,7 @@
:returns: A fully operationnal WSGI application
"""
- config = make_cubicweb_application(cwconfig)
+ config = config_from_cwconfig(cwconfig)
profile = profile or asbool(config.registry.settings.get(
'cubicweb.profile.enable', False))
if profile:
@@ -146,11 +169,24 @@
return wsgi_application_from_cwconfig(cwconfig)
+def pyramid_app(global_config, **settings):
+ """Return a Pyramid WSGI application bound to a CubicWeb repository."""
+ config = Configurator(settings=settings)
+ config.include('cubicweb.pyramid')
+ return config.make_wsgi_app()
+
+
def includeme(config):
"""Set-up a CubicWeb instance.
The CubicWeb instance can be set in several ways:
+ - Provide an already loaded CubicWeb repository in the registry:
+
+ .. code-block:: python
+
+ config.registry['cubicweb.repository'] = your_repo_instance
+
- Provide an already loaded CubicWeb config instance in the registry:
.. code-block:: python
@@ -160,8 +196,24 @@
- Provide an instance name in the pyramid settings with
:confval:`cubicweb.instance`.
+ A CubicWeb repository is instantiated and attached in
+ 'cubicweb.repository' registry key if not already present.
+
+ The CubicWeb instance registry is attached in 'cubicweb.registry' registry
+ key.
"""
cwconfig = config.registry.get('cubicweb.config')
+ repo = config.registry.get('cubicweb.repository')
+
+ if repo is not None:
+ if cwconfig is None:
+ config.registry['cubicweb.config'] = cwconfig = repo.config
+ elif cwconfig is not repo.config:
+ raise ConfigurationError(
+ 'CubicWeb config instance (found in "cubicweb.config" '
+ 'registry key) mismatches with that of the repository '
+ '(registry["cubicweb.repository"])'
+ )
if cwconfig is None:
debugmode = asbool(
@@ -170,15 +222,11 @@
config.registry.settings['cubicweb.instance'], debugmode=debugmode)
config.registry['cubicweb.config'] = cwconfig
- if cwconfig.debugmode:
- try:
- config.include('pyramid_debugtoolbar')
- except ImportError:
- warn('pyramid_debugtoolbar package not available, install it to '
- 'get UI debug features', RuntimeWarning)
+ if repo is None:
+ repo = config.registry['cubicweb.repository'] = cwconfig.repository()
+ config.registry['cubicweb.registry'] = repo.vreg
- config.registry['cubicweb.repository'] = repo = cwconfig.repository()
- config.registry['cubicweb.registry'] = repo.vreg
+ atexit.register(repo.shutdown)
if asbool(config.registry.settings.get('cubicweb.defaults', True)):
config.include('cubicweb.pyramid.defaults')
@@ -186,10 +234,14 @@
for name in aslist(config.registry.settings.get('cubicweb.includes', [])):
config.include(name)
- config.include('cubicweb.pyramid.tools')
- config.include('cubicweb.pyramid.predicates')
config.include('cubicweb.pyramid.core')
- config.include('cubicweb.pyramid.syncsession')
if asbool(config.registry.settings.get('cubicweb.bwcompat', True)):
config.include('cubicweb.pyramid.bwcompat')
+
+ if cwconfig.debugmode:
+ try:
+ config.include('pyramid_debugtoolbar')
+ except ImportError:
+ warn('pyramid_debugtoolbar package not available, install it to '
+ 'get UI debug features', RuntimeWarning)
--- a/cubicweb/pyramid/auth.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/auth.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,25 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
+"""Authentication policies for cubicweb.pyramid."""
+
import datetime
import logging
import warnings
@@ -106,20 +128,10 @@
session_prefix = 'cubicweb.auth.authtkt.session.'
persistent_prefix = 'cubicweb.auth.authtkt.persistent.'
- try:
- secret = config.registry['cubicweb.config']['pyramid-auth-secret']
- warnings.warn(
- "pyramid-auth-secret from all-in-one is now "
- "cubicweb.auth.authtkt.[session|persistent].secret",
- DeprecationWarning)
- except:
- secret = 'notsosecret'
-
session_secret = settings.get(
- session_prefix + 'secret', secret)
+ session_prefix + 'secret', 'notsosecret')
persistent_secret = settings.get(
- persistent_prefix + 'secret', secret)
-
+ persistent_prefix + 'secret', 'notsosecret')
if 'notsosecret' in (session_secret, persistent_secret):
warnings.warn('''
--- a/cubicweb/pyramid/bwcompat.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/bwcompat.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,25 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
+"""Backward compatibility layer for CubicWeb to run as a Pyramid application."""
+
import sys
import logging
@@ -53,10 +75,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 +188,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:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/config.py Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,64 @@
+# 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 <http://www.gnu.org/licenses/>.
+"""Configuration for CubicWeb instances on top of a Pyramid application"""
+
+from os import path
+import random
+import string
+
+from logilab.common.configuration import merge_options
+
+from cubicweb.cwconfig import CONFIGURATIONS
+from cubicweb.server.serverconfig import ServerConfiguration
+from cubicweb.toolsutils import fill_templated_file
+from cubicweb.web.webconfig import BaseWebConfiguration
+
+
+def get_random_secret_key():
+ """Return 50-character secret string"""
+ chars = string.ascii_letters + string.digits
+ return "".join([random.choice(chars) for i in range(50)])
+
+
+class CubicWebPyramidConfiguration(BaseWebConfiguration, ServerConfiguration):
+ """Pyramid application with a CubicWeb repository"""
+ name = 'pyramid'
+
+ cubicweb_appobject_path = (BaseWebConfiguration.cubicweb_appobject_path
+ | ServerConfiguration.cubicweb_appobject_path)
+ cube_appobject_path = (BaseWebConfiguration.cube_appobject_path
+ | ServerConfiguration.cube_appobject_path)
+
+ options = merge_options(ServerConfiguration.options +
+ BaseWebConfiguration.options)
+
+ def write_development_ini(self, cubes):
+ """Write a 'development.ini' file into apphome."""
+ template_fpath = path.join(path.dirname(__file__), 'development.ini.tmpl')
+ target_fpath = path.join(self.apphome, 'development.ini')
+ context = {
+ 'instance': self.appid,
+ 'cubename': cubes[0],
+ 'session-secret': get_random_secret_key(),
+ 'auth-authtkt-persistent-secret': get_random_secret_key(),
+ 'auth-authtkt-session-secret': get_random_secret_key(),
+ }
+ fill_templated_file(template_fpath, target_fpath, context)
+
+
+CONFIGURATIONS.append(CubicWebPyramidConfiguration)
--- a/cubicweb/pyramid/core.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/core.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,25 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
+"""Binding of CubicWeb connection to Pyramid request."""
+
import itertools
from contextlib import contextmanager
@@ -30,12 +52,12 @@
"""
def __init__(self, session, *args, **kw):
- super(Connection, self).__init__(session, *args, **kw)
- self._session = session
+ super(Connection, self).__init__(session._repo, session._user, *args, **kw)
+ self.session = session
self.lang = session._cached_lang
def _get_session_data(self):
- return self._session.data
+ return self.session.data
def _set_session_data(self, data):
pass
@@ -43,15 +65,26 @@
_session_data = property(_get_session_data, _set_session_data)
-class Session(cwsession.Session):
+class Session(object):
""" A Session that access the session data through a property.
Along with :class:`Connection`, it avoid any load of the pyramid session
data until it is actually accessed.
"""
def __init__(self, pyramid_request, user, repo):
- super(Session, self).__init__(user, repo)
self._pyramid_request = pyramid_request
+ self._user = user
+ self._repo = repo
+
+ @property
+ def anonymous_session(self):
+ # XXX for now, anonymous_user only exists in webconfig (and testconfig).
+ # It will only be present inside all-in-one instance.
+ # there is plan to move it down to global config.
+ if not hasattr(self._repo.config, 'anonymous_user'):
+ # not a web or test config, no anonymous user
+ return False
+ return self._user.login == self._repo.config.anonymous_user()[0]
def get_data(self):
if not getattr(self, '_protect_data_access', False):
@@ -126,12 +159,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 +189,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 +242,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']
@@ -277,12 +306,6 @@
session = Session(request, user, repo)
session._cached_lang = lang
tools.cnx_attach_entity(session, user)
- # Calling the hooks should be done only once, disabling it completely for
- # now
- # with session.new_cnx() as cnx:
- # repo.hm.call_hooks('session_open', cnx)
- # cnx.commit()
- # repo._sessions[session.sessionid] = session
return session
--- a/cubicweb/pyramid/defaults.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/defaults.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,23 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
""" Defaults for a classical CubicWeb instance. """
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/development.ini.tmpl Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,41 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:cubicweb#main
+
+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
+ cubicweb_%(cubename)s
+
+# 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 = false
+cubicweb.debug = true
+cubicweb.session.secret = %(session-secret)s
+cubicweb.auth.authtkt.persistent.secure = false
+cubicweb.auth.authtkt.persistent.secret = %(auth-authtkt-persistent-secret)s
+cubicweb.auth.authtkt.session.secure = false
+cubicweb.auth.authtkt.session.secret = %(auth-authtkt-session-secret)s
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = 127.0.0.1:6543 [::1]:6543
--- a/cubicweb/pyramid/init_instance.py Mon Mar 20 09:40:24 2017 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-from cubicweb.cwconfig import CubicWebConfiguration
-
-
-def includeme(config):
- appid = config.registry.settings['cubicweb.instance']
- cwconfig = CubicWebConfiguration.config_for(appid)
-
- config.registry['cubicweb.config'] = cwconfig
- config.registry['cubicweb.repository'] = repo = cwconfig.repository()
- config.registry['cubicweb.registry'] = repo.vreg
--- a/cubicweb/pyramid/login.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/login.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,23 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
""" Provide login views that reproduce a classical CubicWeb behavior"""
from pyramid import security
from pyramid.httpexceptions import HTTPSeeOther
--- a/cubicweb/pyramid/predicates.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/predicates.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,23 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
"""Contains predicates used in Pyramid views.
"""
--- a/cubicweb/pyramid/profile.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/profile.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,23 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
""" Tools for profiling.
See :ref:`profiling`."""
--- a/cubicweb/pyramid/pyramidctl.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/pyramidctl.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,9 +1,30 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
"""
Provides a 'pyramid' command as a replacement to the 'start' command.
The reloading strategy is heavily inspired by (and partially copied from)
the pyramid script 'pserve'.
"""
+
from __future__ import print_function
import atexit
@@ -14,12 +35,14 @@
import time
import threading
import subprocess
+import warnings
-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
-from cubicweb.server import set_debug
+from cubicweb.server import serverctl, set_debug
+from cubicweb.web.webctl import WebCreateHandler
import waitress
@@ -29,6 +52,17 @@
LOG_LEVELS = ('debug', 'info', 'warning', 'error')
+class PyramidCreateHandler(serverctl.RepositoryCreateHandler,
+ WebCreateHandler):
+ cfgname = 'pyramid'
+
+ def bootstrap(self, cubes, automatic=False, inputlevel=0):
+ serverctl.RepositoryCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
+ # Call WebCreateHandler.bootstrap to prompt about get anonymous-user.
+ WebCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
+ self.config.write_development_ini(cubes)
+
+
class PyramidStartHandler(InstanceCommand):
"""Start an interactive pyramid server.
@@ -99,15 +133,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
@@ -326,8 +351,11 @@
host = cwconfig['interface']
port = cwconfig['port'] or 8080
repo = app.application.registry['cubicweb.repository']
+ warnings.warn(
+ 'the "pyramid" command does not start repository "looping tasks" '
+ 'anymore; use the standalone "scheduler" command if needed'
+ )
try:
- repo.start_looping_tasks()
waitress.serve(app, host=host, port=port)
finally:
repo.shutdown()
--- a/cubicweb/pyramid/resources.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/resources.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,5 +1,25 @@
-"""Contains resources classes.
-"""
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
+"""Pyramid resource definitions for CubicWeb."""
+
from six import text_type
from rql import TypeResolverException
--- a/cubicweb/pyramid/rest_api.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/rest_api.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,25 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
+"""Experimental REST API for CubicWeb using Pyramid."""
+
from __future__ import absolute_import
@@ -16,6 +38,7 @@
def includeme(config):
+ config.include('.predicates')
config.add_route(
'cwentities', '/{etype}/*traverse',
factory=ETypeResource.from_match('etype'), match_is_etype='etype')
--- a/cubicweb/pyramid/session.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/session.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,25 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
+"""Pyramid session factories for CubicWeb."""
+
import warnings
import logging
from contextlib import contextmanager
@@ -165,15 +187,9 @@
See also :ref:`defaults_module`
"""
settings = config.registry.settings
- secret = settings.get('cubicweb.session.secret', '')
- if not secret:
- secret = config.registry['cubicweb.config'].get('pyramid-session-secret')
- warnings.warn('''
- Please migrate pyramid-session-secret from
- all-in-one.conf to cubicweb.session.secret config entry in
- your pyramid.ini file.
- ''')
- if not secret:
+ try:
+ secret = settings['cubicweb.session.secret']
+ except KeyError:
secret = 'notsosecret'
warnings.warn('''
--- a/cubicweb/pyramid/syncsession.py Mon Mar 20 09:40:24 2017 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-# copyright 2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Override cubicweb's syncsession hooks to handle them in the pyramid's way"""
-
-from logilab.common.decorators import monkeypatch
-from cubicweb.hooks import syncsession
-
-
-def includeme(config):
-
- @monkeypatch(syncsession)
- def get_user_sessions(cnx, user_eid):
- if cnx.user.eid == user_eid:
- yield cnx
--- a/cubicweb/pyramid/test/__init__.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/test/__init__.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,9 +1,8 @@
import webtest
+from pyramid.config import Configurator
from cubicweb.devtools.webtest import CubicWebTestTC
-from cubicweb.pyramid import make_cubicweb_application
-
class PyramidCWTest(CubicWebTestTC):
settings = {}
@@ -11,15 +10,14 @@
@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
super(CubicWebTestTC, self).setUp()
- config = make_cubicweb_application(self.config, self.settings)
+ config = Configurator(settings=self.settings)
+ config.registry['cubicweb.repository'] = self.repo
+ config.include('cubicweb.pyramid')
self.includeme(config)
self.pyr_registry = config.registry
self.webapp = webtest.TestApp(
--- a/cubicweb/pyramid/test/test_bw_request.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/test/test_bw_request.py Mon Mar 20 10:28:01 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/test/test_config.py Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,64 @@
+# 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 <http://www.gnu.org/licenses/>.
+"""Tests for cubicweb.pyramid.config module."""
+
+import os
+from os import path
+from unittest import TestCase
+
+from mock import patch
+
+from cubicweb.devtools.testlib import TemporaryDirectory
+
+from cubicweb.pyramid import config
+
+
+class PyramidConfigTC(TestCase):
+
+ def test_get_random_secret_key(self):
+ with patch('random.choice', return_value='0') as patched_choice:
+ secret = config.get_random_secret_key()
+ self.assertEqual(patched_choice.call_count, 50)
+ self.assertEqual(secret, '0' * 50)
+
+ def test_write_development_ini(self):
+ with TemporaryDirectory() as instancedir:
+ appid = 'pyramid-instance'
+ os.makedirs(path.join(instancedir, appid))
+ os.environ['CW_INSTANCES_DIR'] = instancedir
+ try:
+ cfg = config.CubicWebPyramidConfiguration(appid)
+ with patch('random.choice', return_value='0') as patched_choice:
+ cfg.write_development_ini(['foo', 'bar'])
+ finally:
+ os.environ.pop('CW_INSTANCES_DIR')
+ with open(path.join(instancedir, appid, 'development.ini')) as f:
+ lines = f.readlines()
+ self.assertEqual(patched_choice.call_count, 50 * 3)
+ secret = '0' * 50
+ for option in ('cubicweb.session.secret',
+ 'cubicweb.auth.authtkt.persistent.secret',
+ 'cubicweb.auth.authtkt.session.secret'):
+ self.assertIn('{} = {}\n'.format(option, secret), lines)
+ self.assertIn('cubicweb.instance = {}\n'.format(appid), lines)
+ self.assertIn(' cubicweb_foo\n', lines)
+
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
--- a/cubicweb/pyramid/test/test_login.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/test/test_login.py Mon Mar 20 10:28:01 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):
--- a/cubicweb/pyramid/tools.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/pyramid/tools.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,23 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2014-2016 UNLISH S.A.S. (Montpellier, FRANCE), all rights reserved.
+#
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
"""Various tools.
.. warning::
@@ -6,11 +26,7 @@
with caution, as the API may change without notice.
"""
-#: A short-term cache for user clones.
-#: used by cached_build_user to speed-up repetitive calls to build_user
-#: The expiration is handled in a dumb and brutal way: the whole cache is
-#: cleared every 5 minutes.
-_user_cache = {}
+from repoze.lru import lru_cache
def clone_user(repo, user):
@@ -39,34 +55,13 @@
entity.cw_rset.req = cnx
+@lru_cache(10)
def cached_build_user(repo, eid):
"""Cached version of
:meth:`cubicweb.server.repository.Repository._build_user`
"""
- if eid in _user_cache:
- user, lang = _user_cache[eid]
- entity = clone_user(repo, user)
- return entity, lang
-
with repo.internal_cnx() as cnx:
user = repo._build_user(cnx, eid)
lang = user.prefered_language()
user.cw_clear_relation_cache()
- _user_cache[eid] = (clone_user(repo, user), lang)
- return user, lang
-
-
-def clear_cache():
- """Clear the user cache"""
- _user_cache.clear()
-
-
-def includeme(config):
- """Start the cache maintenance loop task.
-
- Automatically included by :func:`cubicweb.pyramid.make_cubicweb_application`.
- """
- repo = config.registry['cubicweb.repository']
- interval = int(config.registry.settings.get(
- 'cubicweb.usercache.expiration_time', 60 * 5))
- repo.looping_task(interval, clear_cache)
+ return clone_user(repo, user), lang
--- a/cubicweb/repoapi.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/repoapi.py Mon Mar 20 10:28:01 2017 +0100
@@ -15,21 +15,18 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Official API to access the content of a repository
-"""
+"""Official API to access the content of a repository."""
+
from warnings import warn
from six import add_metaclass
from logilab.common.deprecation import class_deprecated
-from cubicweb.utils import parse_repo_uri
from cubicweb import AuthenticationError
from cubicweb.server.session import Connection
-### public API ######################################################
-
def get_repository(uri=None, config=None, vreg=None):
"""get a repository for the given URI or config/vregistry (in case we're
loading the repository for a client, eg web server, configuration).
@@ -43,11 +40,17 @@
assert config is not None, 'get_repository(config=config)'
return config.repository(vreg)
+
def connect(repo, login, **kwargs):
"""Take credential and return associated Connection.
- raise AuthenticationError if the credential are invalid."""
- return repo.new_session(login, **kwargs).new_cnx()
+ raise AuthenticationError if the credential are invalid.
+ """
+ # use an internal connection to try to get a user object
+ with repo.internal_cnx() as cnx:
+ user = repo.authenticate_user(cnx, login, **kwargs)
+ return Connection(repo, user)
+
def anonymous_cnx(repo):
"""return a Connection for Anonymous user.
@@ -55,7 +58,7 @@
raises an AuthenticationError if anonymous usage is not allowed
"""
anoninfo = getattr(repo.config, 'anonymous_user', lambda: None)()
- if anoninfo is None: # no anonymous user
+ if anoninfo is None: # no anonymous user
raise AuthenticationError('anonymous access is not authorized')
anon_login, anon_password = anoninfo
# use vreg's repository cache
--- a/cubicweb/req.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/req.py Mon Mar 20 10:28:01 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('/') + '/'
--- a/cubicweb/rqlrewrite.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/rqlrewrite.py Mon Mar 20 10:28:01 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',
--- a/cubicweb/rset.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/rset.py Mon Mar 20 10:28:01 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,14 +20,14 @@
from warnings import warn
-from six import PY3
+from six import PY3, text_type
from six.moves import range
from logilab.common import nullobject
from logilab.common.decorators import cached, clear_cache, copy_cache
from rql import nodes, stmts
-from cubicweb import NotAnEntity, NoResultError, MultipleResultsError
+from cubicweb import NotAnEntity, NoResultError, MultipleResultsError, UnknownEid
_MARKER = 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
@@ -560,11 +560,21 @@
@cached
def syntax_tree(self):
- """return the syntax tree (:class:`rql.stmts.Union`) for the
- originating query. You can expect it to have solutions
- computed and it will be properly annotated.
+ """Return the **cached** syntax tree (:class:`rql.stmts.Union`) for the
+ originating query.
+
+ You can expect it to have solutions computed and it will be properly annotated.
+ Since this is a cached shared object, **you must not modify it**.
"""
- return self.req.vreg.parse(self.req, self.rql, self.args)
+ cnx = getattr(self.req, 'cnx', self.req)
+ try:
+ rqlst = cnx.repo.querier.rql_cache.get(cnx, self.rql, self.args)[0]
+ if not rqlst.annotated:
+ self.req.vreg.rqlhelper.annotate(rqlst)
+ return rqlst
+ except UnknownEid:
+ # unknown eid in args prevent usage of rql cache, but we still need a rql st
+ return self.req.vreg.parse(self.req, self.rql, self.args)
@cached
def column_types(self, col):
@@ -592,7 +602,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 +675,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 +693,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
--- a/cubicweb/rtags.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/rtags.py Mon Mar 20 10:28:01 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):
--- a/cubicweb/schema.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/schema.py Mon Mar 20 10:28:01 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 <directory>
"""
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 <directory>
"""
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
--- a/cubicweb/server/__init__.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/__init__.py Mon Mar 20 10:28:01 2017 +0100
@@ -22,8 +22,6 @@
"""
from __future__ import print_function
-
-
from contextlib import contextmanager
from six import text_type, string_types
@@ -39,12 +37,6 @@
from cubicweb.appobject import AppObject
-class ShuttingDown(BaseException):
- """raised when trying to access some resources while the repository is
- shutting down. Inherit from BaseException so that `except Exception` won't
- catch it.
- """
-
# server-side services #########################################################
class Service(AppObject):
@@ -68,16 +60,16 @@
# server debugging flags. They may be combined using binary operators.
-#:no debug information
+#: no debug information
DBG_NONE = 0 #: no debug information
#: rql execution information
-DBG_RQL = 1
+DBG_RQL = 1
#: executed sql
-DBG_SQL = 2
+DBG_SQL = 2
#: repository events
DBG_REPO = 4
#: multi-sources
-DBG_MS = 8
+DBG_MS = 8
#: hooks
DBG_HOOKS = 16
#: operations
@@ -206,7 +198,7 @@
def init_repository(config, interactive=True, drop=False, vreg=None,
init_config=None):
- """initialise a repository database by creating tables add filling them
+ """Initialise a repository database by creating tables and filling them
with the minimal set of entities (ie at least the schema, base groups and
a initial user)
"""
@@ -223,6 +215,7 @@
config.cube_appobject_path = set(('hooks', 'entities'))
# only enable the system source at initialization time
repo = Repository(config, vreg=vreg)
+ repo.bootstrap()
if init_config is not None:
# further config initialization once it has been bootstrapped
init_config(config)
--- a/cubicweb/server/checkintegrity.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/checkintegrity.py Mon Mar 20 10:28:01 2017 +0100
@@ -140,6 +140,19 @@
pb.finish()
+_CHECKERS = {}
+
+
+def _checker(func):
+ """Decorator to register a function as a checker for check()."""
+ fname = func.__name__
+ prefix = 'check_'
+ assert fname.startswith(prefix), 'cannot register %s as a checker' % func
+ _CHECKERS[fname[len(prefix):]] = func
+ return func
+
+
+@_checker
def check_schema(schema, cnx, eids, fix=1):
"""check serialized schema"""
print('Checking serialized schema')
@@ -157,10 +170,12 @@
print('dunno how to fix, do it yourself')
+@_checker
def check_text_index(schema, cnx, eids, fix=1):
"""check all entities registered in the text index"""
print('Checking text index')
- msg = ' Entity with eid %s exists in the text index but in no source (autofix will remove from text index)'
+ msg = (' Entity with eid %s exists in the text index but not in any '
+ 'entity type table (autofix will remove from text index)')
cursor = cnx.system_sql('SELECT uid FROM appears;')
for row in cursor.fetchall():
eid = row[0]
@@ -171,11 +186,13 @@
notify_fixed(fix)
+@_checker
def check_entities(schema, cnx, eids, fix=1):
"""check all entities registered in the repo system table"""
print('Checking entities system table')
# system table but no source
- msg = ' Entity %s with eid %s exists in the system table but in no source (autofix will delete the entity)'
+ msg = (' Entity %s with eid %s exists in "entities" table but not in any '
+ 'entity type table (autofix will delete the entity)')
cursor = cnx.system_sql('SELECT eid,type FROM entities;')
for row in cursor.fetchall():
eid, etype = row
@@ -228,7 +245,8 @@
' WHERE cs.eid_from=e.eid AND cs.eid_to=s.cw_eid)')
notify_fixed(True)
print('Checking entities tables')
- msg = ' Entity with eid %s exists in the %s table but not in the system table (autofix will delete the entity)'
+ msg = (' Entity with eid %s exists in the %s table but not in "entities" '
+ 'table (autofix will delete the entity)')
for eschema in schema.entities():
if eschema.final:
continue
@@ -247,8 +265,9 @@
def bad_related_msg(rtype, target, eid, fix):
- msg = ' A relation %s with %s eid %s exists but no such entity in sources'
- sys.stderr.write(msg % (rtype, target, eid))
+ msg = (' A relation %(rtype)s with %(target)s eid %(eid)d exists but '
+ 'entity #(eid)d does not exist')
+ sys.stderr.write(msg % {'rtype': rtype, 'target': target, 'eid': eid})
notify_fixed(fix)
@@ -259,13 +278,14 @@
notify_fixed(fix)
+@_checker
def check_relations(schema, cnx, eids, fix=1):
"""check that eids referenced by relations are registered in the repo system
table
"""
print('Checking relations')
for rschema in schema.relations():
- if rschema.final or rschema.type in PURE_VIRTUAL_RTYPES:
+ if rschema.final or rschema.rule or rschema.type in PURE_VIRTUAL_RTYPES:
continue
if rschema.inlined:
for subjtype in rschema.subjects():
@@ -308,6 +328,7 @@
cnx.system_sql(sql)
+@_checker
def check_mandatory_relations(schema, cnx, eids, fix=1):
"""check entities missing some mandatory relation"""
print('Checking mandatory relations')
@@ -335,6 +356,7 @@
notify_fixed(fix)
+@_checker
def check_mandatory_attributes(schema, cnx, eids, fix=1):
"""check for entities stored in the system source missing some mandatory
attribute
@@ -355,6 +377,7 @@
notify_fixed(fix)
+@_checker
def check_metadata(schema, cnx, eids, fix=1):
"""check entities has required metadata
@@ -397,7 +420,7 @@
eids_cache = {}
with cnx.security_enabled(read=False, write=False): # ensure no read security
for check in checks:
- check_func = globals()['check_%s' % check]
+ check_func = _CHECKERS[check]
check_func(repo.schema, cnx, eids_cache, fix=fix)
if fix:
cnx.commit()
--- a/cubicweb/server/hook.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/hook.py Mon Mar 20 10:28:01 2017 +0100
@@ -189,9 +189,6 @@
`server_restore`) have a `repo` and a `timestamp` attributes, but
*their `_cw` attribute is None*.
-Hooks called on session event (eg `session_open`, `session_close`) have no
-special attribute.
-
API
---
@@ -272,8 +269,7 @@
'before_delete_relation','after_delete_relation'))
SYSTEM_HOOKS = set(('server_backup', 'server_restore',
'server_startup', 'server_maintenance',
- 'server_shutdown', 'before_server_shutdown',
- 'session_open', 'session_close'))
+ 'server_shutdown', 'before_server_shutdown',))
ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
--- a/cubicweb/server/migractions.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/migractions.py Mon Mar 20 10:28:01 2017 +0100
@@ -58,7 +58,7 @@
from cubicweb.cwvreg import CW_EVENT_MANAGER
from cubicweb import repoapi
from cubicweb.migration import MigrationHelper, yes
-from cubicweb.server import hook, schemaserial as ss
+from cubicweb.server import hook, schemaserial as ss, repository
from cubicweb.server.schema2sql import eschema2sql, rschema2sql, unique_index_name, sql_type
from cubicweb.server.utils import manager_userpasswd
from cubicweb.server.sqlutils import sqlexec, SQL_PREFIX
@@ -96,12 +96,9 @@
assert repo
self.cnx = cnx
self.repo = repo
- self.session = cnx.session
elif connect:
self.repo = config.repository()
self.set_cnx()
- else:
- self.session = None
# no config on shell to a remote instance
if config is not None and (cnx or connect):
repo = self.repo
@@ -154,7 +151,6 @@
except (KeyboardInterrupt, EOFError):
print('aborting...')
sys.exit(0)
- self.session = self.repo._get_session(self.cnx.sessionid)
def cube_upgraded(self, cube, version):
self.cmd_set_property('system.version.%s' % cube.lower(),
@@ -268,8 +264,7 @@
written_format = format_file.readline().strip()
if written_format in ('portable', 'native'):
format = written_format
- self.config.init_cnxset_pool = False
- repo = self.repo = self.config.repository()
+ repo = self.repo = repository.Repository(self.config)
source = repo.system_source
try:
source.restore(osp.join(tmpdir, source.uri), self.confirm, drop, format)
@@ -277,9 +272,10 @@
print('-> error trying to restore %s [%s]' % (source.uri, exc))
if not self.confirm('Continue anyway?', default='n'):
raise SystemExit(1)
- shutil.rmtree(tmpdir)
+ finally:
+ shutil.rmtree(tmpdir)
# call hooks
- repo.init_cnxset_pool()
+ repo.bootstrap()
repo.hm.call_hooks('server_restore', repo=repo, timestamp=backupfile)
print('-> database restored.')
--- a/cubicweb/server/querier.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/querier.py Mon Mar 20 10:28:01 2017 +0100
@@ -23,14 +23,14 @@
from itertools import repeat
from six import text_type, string_types, integer_types
-from six.moves import range
+from six.moves import range, zip
from rql import RQLSyntaxError, CoercionError
from rql.stmts import Union
from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj, Relation, Exists, Not
from yams import BASE_TYPES
-from cubicweb import ValidationError, Unauthorized, UnknownEid
+from cubicweb import ValidationError, Unauthorized, UnknownEid, QueryError
from cubicweb.rqlrewrite import RQLRelationRewriter
from cubicweb import Binary, server
from cubicweb.rset import ResultSet
@@ -477,33 +477,23 @@
def set_schema(self, schema):
self.schema = schema
- repo = self._repo
- # rql st and solution cache.
- self._rql_cache = QueryCache(repo.config['rql-cache-size'])
- # rql cache key cache. Don't bother using a Cache instance: we should
- # have a limited number of queries in there, since there are no entries
- # in this cache for user queries (which have no args)
- self._rql_ck_cache = {}
- # some cache usage stats
- self.cache_hit, self.cache_miss = 0, 0
- # rql parsing / analysing helper
- self.solutions = repo.vreg.solutions
- rqlhelper = repo.vreg.rqlhelper
- # set backend on the rql helper, will be used for function checking
- rqlhelper.backend = repo.config.system_source_config['db-driver']
- self._parse = rqlhelper.parse
+ self.clear_caches()
+ rqlhelper = self._repo.vreg.rqlhelper
self._annotate = rqlhelper.annotate
# rql planner
self._planner = SSPlanner(schema, rqlhelper)
# sql generation annotator
self.sqlgen_annotate = SQLGenAnnotator(schema).annotate
- def parse(self, rql, annotate=False):
- """return a rql syntax tree for the given rql"""
- try:
- return self._parse(text_type(rql), annotate=annotate)
- except UnicodeError:
- raise RQLSyntaxError(rql)
+ def clear_caches(self, eids=None, etypes=None):
+ if eids is None:
+ self.rql_cache = RQLCache(self._repo, self.schema)
+ else:
+ cache = self.rql_cache
+ for eid, etype in zip(eids, etypes):
+ cache.pop(('Any X WHERE X eid %s' % eid,), None)
+ if etype is not None:
+ cache.pop(('%s X WHERE X eid %s' % (etype, eid),), None)
def plan_factory(self, rqlst, args, cnx):
"""create an execution plan for an INSERT RQL query"""
@@ -535,46 +525,12 @@
if server.DEBUG & (server.DBG_MORE | server.DBG_SQL):
print('*'*80)
print('querier input', repr(rql), repr(args))
- # parse the query and binds variables
- cachekey = (rql,)
try:
- if args:
- # search for named args in query which are eids (hence
- # influencing query's solutions)
- eidkeys = self._rql_ck_cache[rql]
- if eidkeys:
- # if there are some, we need a better cache key, eg (rql +
- # entity type of each eid)
- try:
- cachekey = self._repo.querier_cache_key(cnx, rql,
- args, eidkeys)
- except UnknownEid:
- # we want queries such as "Any X WHERE X eid 9999"
- # return an empty result instead of raising UnknownEid
- return empty_rset(rql, args)
- rqlst = self._rql_cache[cachekey]
- self.cache_hit += 1
- statsd_c('cache_hit')
- except KeyError:
- self.cache_miss += 1
- statsd_c('cache_miss')
- rqlst = self.parse(rql)
- try:
- # compute solutions for rqlst and return named args in query
- # which are eids. Notice that if you may not need `eidkeys`, we
- # have to compute solutions anyway (kept as annotation on the
- # tree)
- eidkeys = self.solutions(cnx, rqlst, args)
- except UnknownEid:
- # we want queries such as "Any X WHERE X eid 9999" return an
- # empty result instead of raising UnknownEid
- return empty_rset(rql, args)
- if args and rql not in self._rql_ck_cache:
- self._rql_ck_cache[rql] = eidkeys
- if eidkeys:
- cachekey = self._repo.querier_cache_key(cnx, rql, args,
- eidkeys)
- self._rql_cache[cachekey] = rqlst
+ rqlst, cachekey = self.rql_cache.get(cnx, rql, args)
+ except UnknownEid:
+ # we want queries such as "Any X WHERE X eid 9999"
+ # return an empty result instead of raising UnknownEid
+ return empty_rset(rql, args)
if rqlst.TYPE != 'select':
if cnx.read_security:
check_no_password_selected(rqlst)
@@ -646,6 +602,92 @@
# only defining here to prevent pylint from complaining
info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
+
+class RQLCache(object):
+
+ def __init__(self, repo, schema):
+ # rql st and solution cache.
+ self._cache = QueryCache(repo.config['rql-cache-size'])
+ # rql cache key cache. Don't bother using a Cache instance: we should
+ # have a limited number of queries in there, since there are no entries
+ # in this cache for user queries (which have no args)
+ self._ck_cache = {}
+ # some cache usage stats
+ self.cache_hit, self.cache_miss = 0, 0
+ # rql parsing / analysing helper
+ self.solutions = repo.vreg.solutions
+ rqlhelper = repo.vreg.rqlhelper
+ # set backend on the rql helper, will be used for function checking
+ rqlhelper.backend = repo.config.system_source_config['db-driver']
+
+ def parse(rql, annotate=False, parse=rqlhelper.parse):
+ """Return a freshly parsed syntax tree for the given RQL."""
+ try:
+ return parse(text_type(rql), annotate=annotate)
+ except UnicodeError:
+ raise RQLSyntaxError(rql)
+ self._parse = parse
+
+ def __len__(self):
+ return len(self._cache)
+
+ def get(self, cnx, rql, args):
+ """Return syntax tree and cache key for the given RQL.
+
+ Returned syntax tree is cached and must not be modified
+ """
+ # parse the query and binds variables
+ cachekey = (rql,)
+ try:
+ if args:
+ # search for named args in query which are eids (hence
+ # influencing query's solutions)
+ eidkeys = self._ck_cache[rql]
+ if eidkeys:
+ # if there are some, we need a better cache key, eg (rql +
+ # entity type of each eid)
+ cachekey = _rql_cache_key(cnx, rql, args, eidkeys)
+ rqlst = self._cache[cachekey]
+ self.cache_hit += 1
+ statsd_c('cache_hit')
+ except KeyError:
+ self.cache_miss += 1
+ statsd_c('cache_miss')
+ rqlst = self._parse(rql)
+ # compute solutions for rqlst and return named args in query
+ # which are eids. Notice that if you may not need `eidkeys`, we
+ # have to compute solutions anyway (kept as annotation on the
+ # tree)
+ eidkeys = self.solutions(cnx, rqlst, args)
+ if args and rql not in self._ck_cache:
+ self._ck_cache[rql] = eidkeys
+ if eidkeys:
+ cachekey = _rql_cache_key(cnx, rql, args, eidkeys)
+ self._cache[cachekey] = rqlst
+ return rqlst, cachekey
+
+ def pop(self, key, *args):
+ """Pop a key from the cache."""
+ self._cache.pop(key, *args)
+
+
+def _rql_cache_key(cnx, rql, args, eidkeys):
+ cachekey = [rql]
+ type_from_eid = cnx.repo.type_from_eid
+ for key in sorted(eidkeys):
+ try:
+ etype = type_from_eid(args[key], cnx)
+ except KeyError:
+ raise QueryError('bad cache key %s (no value)' % key)
+ except TypeError:
+ raise QueryError('bad cache key %s (value: %r)' % (
+ key, args[key]))
+ cachekey.append(etype)
+ # ensure eid is correctly typed in args
+ args[key] = int(args[key])
+ return tuple(cachekey)
+
+
from logging import getLogger
from cubicweb import set_log_methods
LOGGER = getLogger('cubicweb.querier')
--- a/cubicweb/server/repository.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/repository.py Mon Mar 20 10:28:01 2017 +0100
@@ -30,7 +30,6 @@
from warnings import warn
from itertools import chain
-from time import time, localtime, strftime
from contextlib import contextmanager
from logging import getLogger
@@ -42,14 +41,13 @@
from yams import BadSchemaDefinition
from rql.utils import rqlvar_maker
-from cubicweb import (CW_MIGRATION_MAP, QueryError,
+from cubicweb import (CW_MIGRATION_MAP,
UnknownEid, AuthenticationError, ExecutionError,
- BadConnectionId, ValidationError, Unauthorized,
- UniqueTogetherError, onevent, ViolatedConstraint)
+ UniqueTogetherError, ViolatedConstraint)
from cubicweb import set_log_methods
from cubicweb import cwvreg, schema, server
-from cubicweb.server import ShuttingDown, utils, hook, querier, sources
-from cubicweb.server.session import Session, InternalManager
+from cubicweb.server import utils, hook, querier, sources
+from cubicweb.server.session import InternalManager, Connection
NO_CACHE_RELATIONS = set([
@@ -150,23 +148,76 @@
pass
+class _CnxSetPool(object):
+
+ def __init__(self, source, size):
+ self._cnxsets = []
+ if size is not None:
+ self._queue = queue.Queue()
+ for i in range(size):
+ cnxset = source.wrapped_connection()
+ self._cnxsets.append(cnxset)
+ self._queue.put_nowait(cnxset)
+ else:
+ self._queue = None
+ self._source = source
+ super(_CnxSetPool, self).__init__()
+
+ def qsize(self):
+ q = self._queue
+ if q is None:
+ return None
+ return q.qsize()
+
+ def get(self):
+ q = self._queue
+ if q is None:
+ return self._source.wrapped_connection()
+ 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):
+ q = self._queue
+ if q is None:
+ cnxset.close(True)
+ else:
+ self._queue.put_nowait(cnxset)
+
+ def __iter__(self):
+ for cnxset in self._cnxsets:
+ yield cnxset
+
+ def close(self):
+ q = self._queue
+ if q is not None:
+ 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
"""
- def __init__(self, config, tasks_manager=None, vreg=None):
+ def __init__(self, config, scheduler=None, vreg=None):
self.config = config
self.sources_by_eid = {}
if vreg is None:
vreg = cwvreg.CWRegistryStore(config)
self.vreg = vreg
- self._tasks_manager = tasks_manager
+ self._scheduler = scheduler
self.app_instances_bus = NullEventBus()
- self.info('starting repository from %s', self.config.apphome)
- # dictionary of opened sessions
- self._sessions = {}
# list of functions to be called at regular interval
# list of running threads
@@ -175,7 +226,7 @@
self.schema = schema.CubicWebSchema(config.appid)
self.vreg.schema = self.schema # until actual schema is loaded...
# shutdown flag
- self.shutting_down = False
+ self.shutting_down = None
# sources (additional sources info in the system database)
self.system_source = self.get_source('native', 'system',
config.system_source_config.copy())
@@ -184,34 +235,23 @@
self.querier = querier.QuerierHelper(self, self.schema)
# cache eid -> type
self._type_cache = {}
- # open some connection sets
- if config.init_cnxset_pool:
- self.init_cnxset_pool()
# the hooks manager
self.hm = hook.HooksManager(self.vreg)
- # registry hook to fix user class on registry reload
- @onevent('after-registry-reload', self)
- def fix_user_classes(self):
- # After registry reload the 'CWUser' class used for CWEtype
- # changed. So any existing user object have a different class than
- # the new loaded one. We are hot fixing this.
- usercls = self.vreg['etypes'].etype_class('CWUser')
- for session in self._sessions.values():
- if not isinstance(session.user, InternalManager):
- session.user.__class__ = usercls
-
- def init_cnxset_pool(self):
- """should be called bootstrap_repository, as this is what it does"""
+ def bootstrap(self):
+ self.info('starting repository from %s', self.config.apphome)
+ self.shutting_down = False
config = self.config
# copy pool size here since config.init_cube() and config.load_schema()
# reload configuration from file and could reset a manually set pool
# size.
- pool_size = config['connections-pool-size']
- self._cnxsets_pool = queue.Queue()
+ if config['connections-pooler-enabled']:
+ pool_size, min_pool_size = config['connections-pool-size'], 1
+ else:
+ pool_size = min_pool_size = None
# 0. init a cnxset 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, min_pool_size)
# 1. set used cubes
if config.creating or not config.read_instance_schema:
config.bootstrap_cubes()
@@ -227,12 +267,12 @@
# the registry
config.cube_appobject_path = set(('hooks', 'entities'))
config.cubicweb_appobject_path = set(('hooks', 'entities'))
- # limit connections pool to 1
- pool_size = 1
+ # limit connections pool size
+ pool_size = min_pool_size
if config.quick_start or config.creating or not config.read_instance_schema:
# load schema from the file system
if not config.creating:
- self.info("set fs instance'schema")
+ self.info("set fs instance's schema")
self.set_schema(config.load_schema(expand_cubes=True))
if not config.creating:
# set eids on entities schema
@@ -259,12 +299,10 @@
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)
+ # 5. call instance level initialisation hooks
+ self.hm.call_hooks('server_startup', repo=self)
# internals ###############################################################
@@ -284,9 +322,6 @@
continue
self.add_source(sourceent)
- def _clear_planning_caches(self):
- clear_cache(self, 'source_defs')
-
def add_source(self, sourceent):
try:
source = self.get_source(sourceent.type, sourceent.name,
@@ -306,12 +341,12 @@
source.init(True, sourceent)
else:
source.init(False, sourceent)
- self._clear_planning_caches()
+ self._clear_source_defs_caches()
def remove_source(self, uri):
source = self.sources_by_uri.pop(uri)
del self.sources_by_eid[source.eid]
- self._clear_planning_caches()
+ self._clear_source_defs_caches()
def get_source(self, type, uri, source_config, eid=None):
# set uri and type in source config so it's available through
@@ -348,39 +383,21 @@
raise Exception('Is the database initialised ? (cause: %s)' % ex)
return appschema
- def _prepare_startup(self):
- """Prepare "Repository as a server" for startup.
+ def run_scheduler(self):
+ """Start repository scheduler after preparing the repository for that.
* trigger server startup hook,
- * register session clean up task.
- """
- if not (self.config.creating or self.config.repairing
- or self.config.quick_start):
- # call instance level initialisation hooks
- self.hm.call_hooks('server_startup', repo=self)
- # register a task to cleanup expired session
- self.cleanup_session_time = self.config['cleanup-session-time'] or 60 * 60 * 24
- assert self.cleanup_session_time > 0
- cleanup_session_interval = min(60 * 60, self.cleanup_session_time / 3)
- assert self._tasks_manager is not None, \
- "This Repository is not intended to be used as a server"
- self._tasks_manager.add_looping_task(cleanup_session_interval,
- self.clean_sessions)
-
- def start_looping_tasks(self):
- """Actual "Repository as a server" startup.
-
- * trigger server startup hook,
- * register session clean up task,
- * start all tasks.
+ * start the scheduler *and block*.
XXX Other startup related stuffs are done elsewhere. In Repository
XXX __init__ or in external codes (various server managers).
"""
- self._prepare_startup()
- assert self._tasks_manager is not None,\
+ assert self._scheduler is not None, \
"This Repository is not intended to be used as a server"
- self._tasks_manager.start()
+ self.info(
+ 'starting repository scheduler with tasks: %s',
+ ', '.join(e.action.__name__ for e in self._scheduler.queue))
+ self._scheduler.run()
def looping_task(self, interval, func, *args):
"""register a function to be called every `interval` seconds.
@@ -388,27 +405,17 @@
looping tasks can only be registered during repository initialization,
once done this method will fail.
"""
- assert self._tasks_manager is not None,\
+ assert self._scheduler is not None, \
"This Repository is not intended to be used as a server"
- self._tasks_manager.add_looping_task(interval, func, *args)
+ event = utils.schedule_periodic_task(
+ self._scheduler, interval, func, *args)
+ self.info('scheduled periodic task %s (interval: %.2fs)',
+ event.action.__name__, interval)
def threaded_task(self, func):
"""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
@@ -419,9 +426,8 @@
# then, the system source is still available
self.hm.call_hooks('before_server_shutdown', repo=self)
self.shutting_down = True
+ self.info('shutting down repository')
self.system_source.shutdown()
- if self._tasks_manager is not None:
- self._tasks_manager.stop()
if not (self.config.creating or self.config.repairing
or self.config.quick_start):
self.hm.call_hooks('server_shutdown', repo=self)
@@ -429,15 +435,8 @@
self.info('waiting thread %s...', thread.getName())
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
- hits, misses = self.querier.cache_hit, self.querier.cache_miss
+ self.cnxsets.close()
+ hits, misses = self.querier.rql_cache.cache_hit, self.querier.rql_cache.cache_miss
try:
self.info('rql st cache hit/miss: %s/%s (%s%% hits)', hits, misses,
(hits * 100) / (hits + misses))
@@ -588,6 +587,9 @@
sources[uri] = source.public_config
return sources
+ def _clear_source_defs_caches(self):
+ clear_cache(self, 'source_defs')
+
def properties(self):
"""Return a result set containing system wide properties.
@@ -640,95 +642,39 @@
query_attrs)
return rset.rows
- def new_session(self, login, **kwargs):
- """open a *new* session for a given user
-
- raise `AuthenticationError` if the authentication failed
- raise `ConnectionError` if we can't open a connection
- """
- # use an internal connection
- with self.internal_cnx() as cnx:
- # try to get a user object
- user = self.authenticate_user(cnx, login, **kwargs)
- session = Session(user, self)
- user._cw = user.cw_rset.req = session
- user.cw_clear_relation_cache()
- self._sessions[session.sessionid] = session
- self.info('opened session %s for user %s', session.sessionid, login)
- with session.new_cnx() as cnx:
- self.hm.call_hooks('session_open', cnx)
- # commit connection at this point in case write operation has been
- # done during `session_open` hooks
- cnx.commit()
- return session
-
- @deprecated('[3.23] use .new_session instead (and get a plain session object)')
- def connect(self, login, **kwargs):
- return self.new_session(login, **kwargs).sessionid
-
- @deprecated('[3.23] use session.close() directly')
- def close(self, sessionid):
- self._get_session(sessionid).close()
-
# session handling ########################################################
- def close_sessions(self):
- """close every opened sessions"""
- for session in list(self._sessions.values()):
- session.close()
-
- def clean_sessions(self):
- """close sessions not used since an amount of time specified in the
- configuration
- """
- mintime = time() - self.cleanup_session_time
- self.debug('cleaning session unused since %s',
- strftime('%H:%M:%S', localtime(mintime)))
- nbclosed = 0
- for session in list(self._sessions.values()):
- if session.timestamp < mintime:
- session.close()
- nbclosed += 1
- return nbclosed
-
@contextmanager
def internal_cnx(self):
"""Context manager returning a Connection using internal user which have
every access rights on the repository.
- Beware that unlike the older :meth:`internal_session`, internal
- connections have all hooks beside security enabled.
+ Internal connections have all hooks beside security enabled.
"""
- with Session(InternalManager(), self).new_cnx() as cnx:
+ with Connection(self, InternalManager()) as cnx:
cnx.user._cw = cnx # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone
with cnx.security_enabled(read=False, write=False):
yield cnx
- def _get_session(self, sessionid, txid=None, checkshuttingdown=True):
- """return the session associated with the given session identifier"""
- if checkshuttingdown and self.shutting_down:
- raise ShuttingDown('Repository is shutting down')
- try:
- session = self._sessions[sessionid]
- except KeyError:
- raise BadConnectionId('No such session %s' % sessionid)
- return session
-
# data sources handling ###################################################
# * correspondance between eid and type
# * correspondance between eid and local id (i.e. specific to a given source)
- def clear_caches(self, eids):
- etcache = self._type_cache
- rqlcache = self.querier._rql_cache
- for eid in eids:
- try:
- etype = etcache.pop(int(eid)) # may be a string in some cases
- rqlcache.pop(('%s X WHERE X eid %s' % (etype, eid),), None)
- except KeyError:
- etype = None
- rqlcache.pop(('Any X WHERE X eid %s' % eid,), None)
- self.system_source.clear_eid_cache(eid, etype)
+ def clear_caches(self, eids=None):
+ if eids is None:
+ self._type_cache = {}
+ etypes = None
+ else:
+ etypes = []
+ etcache = self._type_cache
+ for eid in eids:
+ try:
+ etype = etcache.pop(int(eid)) # may be a string in some cases
+ except KeyError:
+ etype = None
+ etypes.append(etype)
+ self.querier.clear_caches(eids, etypes)
+ self.system_source.clear_caches(eids, etypes)
def type_from_eid(self, eid, cnx):
"""Return the type of the entity with id `eid`"""
@@ -743,21 +689,6 @@
self._type_cache[eid] = etype
return etype
- def querier_cache_key(self, cnx, rql, args, eidkeys):
- cachekey = [rql]
- for key in sorted(eidkeys):
- try:
- etype = self.type_from_eid(args[key], cnx)
- except KeyError:
- raise QueryError('bad cache key %s (no value)' % key)
- except TypeError:
- raise QueryError('bad cache key %s (value: %r)' % (
- key, args[key]))
- cachekey.append(etype)
- # ensure eid is correctly typed in args
- args[key] = int(args[key])
- return tuple(cachekey)
-
def add_info(self, cnx, entity, source):
"""add type and source info for an eid into the system table,
and index the entity with the full text index
@@ -788,21 +719,7 @@
rql = 'DELETE X %s Y WHERE X eid IN (%s)' % (rtype, in_eids)
else:
rql = 'DELETE Y %s X WHERE X eid IN (%s)' % (rtype, in_eids)
- try:
- cnx.execute(rql, build_descr=False)
- except ValidationError:
- raise
- except Unauthorized:
- self.exception(
- 'Unauthorized exception while cascading delete for entity %s. '
- 'RQL: %s.\nThis should not happen since security is disabled here.',
- entities, rql)
- raise
- except Exception:
- if self.config.mode == 'test':
- raise
- self.exception('error while cascading delete for entity %s. RQL: %s',
- entities, rql)
+ cnx.execute(rql, build_descr=False)
def init_entity_caches(self, cnx, entity, source):
"""Add entity to connection entities cache and repo's cache."""
--- a/cubicweb/server/serverconfig.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/serverconfig.py Mon Mar 20 10:28:01 2017 +0100
@@ -131,6 +131,11 @@
'of inactivity. Default to 24h.',
'group': 'main', 'level': 3,
}),
+ ('connections-pooler-enabled',
+ {'type': 'yn', 'default': True,
+ 'help': 'enable the connection pooler',
+ 'group': 'main', 'level': 3,
+ }),
('connections-pool-size',
{'type' : 'int',
'default': 4,
@@ -215,10 +220,6 @@
}),
) + CubicWebConfiguration.options)
- # should we init the connections pool (eg connect to sources). This is
- # usually necessary...
- init_cnxset_pool = True
-
# read the schema from the database
read_instance_schema = True
# set this to true to get a minimal repository, for instance to get cubes
--- a/cubicweb/server/serverctl.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/serverctl.py Mon Mar 20 10:28:01 2017 +0100
@@ -37,6 +37,7 @@
from cubicweb.toolsutils import Command, CommandHandler, underline_title
from cubicweb.cwctl import CWCTL, check_options_consistency, ConfigureInstanceCommand
from cubicweb.server import SOURCE_TYPES
+from cubicweb.server import checkintegrity
from cubicweb.server.serverconfig import (
USER_OPTIONS, ServerConfiguration, SourceConfiguration,
ask_source_config, generate_source_config)
@@ -902,12 +903,8 @@
options = (
('checks',
{'short': 'c', 'type': 'csv', 'metavar': '<check list>',
- 'default': ('entities', 'relations',
- 'mandatory_relations', 'mandatory_attributes',
- 'metadata', 'schema', 'text_index'),
- 'help': 'Comma separated list of check to run. By default run all \
-checks, i.e. entities, relations, mandatory_relations, mandatory_attributes, \
-metadata, text_index and schema.'}
+ 'default': sorted(checkintegrity._CHECKERS),
+ 'help': 'Comma separated list of check to run. By default run all checks.'}
),
('autofix',
@@ -930,13 +927,12 @@
)
def run(self, args):
- from cubicweb.server.checkintegrity import check
appid = args[0]
config = ServerConfiguration.config_for(appid)
config.repairing = self.config.force
repo, _cnx = repo_cnx(config)
with repo.internal_cnx() as cnx:
- check(repo, cnx,
+ checkintegrity.check(repo, cnx,
self.config.checks,
self.config.reindex,
self.config.autofix)
@@ -953,11 +949,10 @@
min_args = 1
def run(self, args):
- from cubicweb.server.checkintegrity import check_indexes
config = ServerConfiguration.config_for(args[0])
repo, cnx = repo_cnx(config)
with cnx:
- status = check_indexes(cnx)
+ status = checkintegrity.check_indexes(cnx)
sys.exit(status)
@@ -985,6 +980,45 @@
cnx.commit()
+class RepositorySchedulerCommand(Command):
+ """Start a repository tasks scheduler.
+
+ Initialize a repository and start its tasks scheduler that would run
+ registered "looping tasks".
+
+ This is maintenance command that should be kept running along with a web
+ instance of a CubicWeb WSGI application (e.g. embeded into a Pyramid
+ application).
+
+ <instance>
+ the identifier of the instance
+ """
+ name = 'scheduler'
+ arguments = '<instance>'
+ min_args = max_args = 1
+ options = (
+ ('loglevel',
+ {'short': 'l', 'type': 'choice', 'metavar': '<log level>',
+ 'default': 'info', 'choices': ('debug', 'info', 'warning', 'error')},
+ ),
+ )
+
+ def run(self, args):
+ from cubicweb.cwctl import init_cmdline_log_threshold
+ from cubicweb.server.repository import Repository
+ from cubicweb.server.utils import scheduler
+ config = ServerConfiguration.config_for(args[0])
+ # Log to stdout, since the this command runs in the foreground.
+ config.global_set_option('log-file', None)
+ init_cmdline_log_threshold(config, self['loglevel'])
+ repo = Repository(config, scheduler())
+ repo.bootstrap()
+ try:
+ repo.run_scheduler()
+ finally:
+ repo.shutdown()
+
+
class SynchronizeSourceCommand(Command):
"""Force sources synchronization.
@@ -1095,6 +1129,7 @@
DBDumpCommand, DBRestoreCommand, DBCopyCommand, DBIndexSanityCheckCommand,
AddSourceCommand, CheckRepositoryCommand, RebuildFTICommand,
SynchronizeSourceCommand, SchemaDiffCommand,
+ RepositorySchedulerCommand,
):
CWCTL.register(cmdclass)
--- a/cubicweb/server/session.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/session.py Mon Mar 20 10:28:01 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.
@@ -21,7 +21,6 @@
import functools
import sys
-from time import time
from uuid import uuid4
from warnings import warn
from contextlib import contextmanager
@@ -30,13 +29,11 @@
from six import text_type
from logilab.common.deprecation import deprecated
-from logilab.common.textutils import unormalize
from logilab.common.registry import objectify_predicate
from cubicweb import QueryError, ProgrammingError, schema, server
from cubicweb import set_log_methods
from cubicweb.req import RequestSessionBase
-from cubicweb.utils import make_uid
from cubicweb.rqlrewrite import RQLRewriter
from cubicweb.server.edition import EditedEntity
@@ -111,27 +108,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 <object>.security_enabled instead')
@@ -176,17 +165,12 @@
DEFAULT_SECURITY = object() # evaluated to true by design
-class SessionClosedError(RuntimeError):
- pass
-
-
def _open_only(func):
"""decorator for Connection method that check it is open"""
@functools.wraps(func)
def check_open(cnx, *args, **kwargs):
if not cnx._open:
- raise ProgrammingError('Closed Connection: %s'
- % cnx.connectionid)
+ raise ProgrammingError('Closed Connection: %s' % cnx)
return func(cnx, *args, **kwargs)
return check_open
@@ -240,13 +224,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:
@@ -257,37 +236,34 @@
is_request = False
hooks_in_progress = False
- def __init__(self, session):
- super(Connection, self).__init__(session.repo.vreg)
+ def __init__(self, repo, user):
+ super(Connection, self).__init__(repo.vreg)
#: connection unique id
self._open = None
- self.connectionid = '%s-%s' % (session.sessionid, uuid4().hex)
- self.session = session
- self.sessionid = session.sessionid
#: server.Repository object
- self.repo = session.repo
+ self.repo = repo
self.vreg = self.repo.vreg
self._execute = self.repo.querier.execute
- # other session utility
- self._session_timestamp = session._timestamp
-
# internal (root) session
- self.is_internal_session = isinstance(session.user, InternalManager)
+ self.is_internal_session = isinstance(user, InternalManager)
#: dict containing arbitrary data cleared at the end of the transaction
self.transaction_data = {}
- self._session_data = session.data
#: ordered list of operations to be processed on commit/rollback
self.pending_operations = []
#: (None, 'precommit', 'postcommit', 'uncommitable')
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
@@ -295,7 +271,7 @@
self.write_security = DEFAULT_SECURITY
# undo control
- config = session.repo.config
+ config = repo.config
if config.creating or config.repairing or self.is_internal_session:
self.undo_actions = False
else:
@@ -305,20 +281,20 @@
self._rewriter = RQLRewriter(self)
# other session utility
- if session.user.login == '__internal_manager__':
- self.user = session.user
+ if user.login == '__internal_manager__':
+ self.user = user
else:
- self._set_user(session.user)
+ self._set_user(user)
@_open_only
def get_schema(self):
"""Return the schema currently used by the repository."""
- return self.session.repo.source_defs()
+ return self.repo.source_defs()
@_open_only
def get_option_value(self, option):
"""Return the value for `option` in the configuration."""
- return self.session.repo.get_option_value(option)
+ return self.repo.get_option_value(option)
# transaction api
@@ -385,7 +361,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 +371,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
@@ -414,8 +390,9 @@
# shared data handling ###################################################
@property
+ @deprecated('[3.25] use transaction_data or req.session.data', stacklevel=3)
def data(self):
- return self._session_data
+ return self.transaction_data
@property
def rql_rewriter(self):
@@ -428,7 +405,7 @@
if txdata:
data = self.transaction_data
else:
- data = self._session_data
+ data = self.data
if pop:
return data.pop(key, default)
else:
@@ -441,7 +418,7 @@
if txdata:
self.transaction_data[key] = value
else:
- self._session_data[key] = value
+ self.data[key] = value
def clear(self):
"""reset internal data"""
@@ -472,10 +449,6 @@
def ensure_cnx_set(self):
yield
- @property
- def anonymous_connection(self):
- return self.session.anonymous_session
-
# Entity cache management #################################################
#
# The connection entity cache as held in cnx.transaction_data is removed at the
@@ -491,7 +464,7 @@
# XXX not using _open_only because before at creation time. _set_user
# call this function to cache the Connection user.
if entity.cw_etype != 'CWUser' and not self._open:
- raise ProgrammingError('Closed Connection: %s' % self.connectionid)
+ raise ProgrammingError('Closed Connection: %s' % self)
ecache = self.transaction_data.setdefault('ecache', {})
ecache.setdefault(entity.eid, entity)
@@ -506,14 +479,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 +649,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):
@@ -802,10 +738,8 @@
See :meth:`cubicweb.dbapi.Cursor.execute` documentation.
"""
- self._session_timestamp.touch()
rset = self._execute(self, rql, kwargs, build_descr)
rset.req = self
- self._session_timestamp.touch()
return rset
@_open_only
@@ -830,9 +764,8 @@
self.critical('rollback error', exc_info=sys.exc_info())
continue
cnxset.rollback()
- self.debug('rollback for transaction %s done', self.connectionid)
+ self.debug('rollback for transaction %s done', self)
finally:
- self._session_timestamp.touch()
self.clear()
@_open_only
@@ -877,7 +810,7 @@
print(operation)
operation.handle_event('precommit_event')
self.pending_operations[:] = processed
- self.debug('precommit transaction %s done', self.connectionid)
+ self.debug('precommit transaction %s done', self)
except BaseException:
# if error on [pre]commit:
#
@@ -921,10 +854,9 @@
except BaseException:
self.critical('error while postcommit',
exc_info=sys.exc_info())
- self.debug('postcommit transaction %s done', self.connectionid)
+ self.debug('postcommit transaction %s done', self)
return self.transaction_uuid(set=False)
finally:
- self._session_timestamp.touch()
self.clear()
# resource accessors ######################################################
@@ -955,129 +887,6 @@
return self.vreg.schema.rschema(rtype).rdefs[(subjtype, objtype)]
-def cnx_attr(attr_name, writable=False):
- """return a property to forward attribute access to connection.
-
- This is to be used by session"""
- args = {}
-
- @deprecated('[3.19] use a Connection object instead')
- def attr_from_cnx(session):
- return getattr(session._cnx, attr_name)
-
- args['fget'] = attr_from_cnx
- if writable:
- @deprecated('[3.19] use a Connection object instead')
- def write_attr(session, value):
- return setattr(session._cnx, attr_name, value)
- args['fset'] = write_attr
- return property(**args)
-
-
-class Timestamp(object):
-
- def __init__(self):
- self.value = time()
-
- def touch(self):
- self.value = time()
-
- def __float__(self):
- return float(self.value)
-
-
-class Session(object):
- """Repository user session
-
- This ties all together:
- * session id,
- * user,
- * other session data.
- """
-
- def __init__(self, user, repo, _id=None):
- self.sessionid = _id or make_uid(unormalize(user.login))
- self.user = user # XXX repoapi: deprecated and store only a login.
- self.repo = repo
- self._timestamp = Timestamp()
- self.data = {}
- self.closed = False
-
- def close(self):
- if self.closed:
- self.warning('closing already closed session %s', self.sessionid)
- return
- with self.new_cnx() as cnx:
- self.repo.hm.call_hooks('session_close', cnx)
- cnx.commit()
- del self.repo._sessions[self.sessionid]
- self.closed = True
- self.info('closed session %s for user %s', self.sessionid, self.user.login)
-
- def __unicode__(self):
- return '<session %s (%s 0x%x)>' % (
- unicode(self.user.login), self.sessionid, id(self))
-
- @property
- def timestamp(self):
- return float(self._timestamp)
-
- @property
- @deprecated('[3.19] session.id is deprecated, use session.sessionid')
- def id(self):
- return self.sessionid
-
- @property
- def login(self):
- return self.user.login
-
- def new_cnx(self):
- """Return a new Connection object linked to the session
-
- The returned Connection will *not* be managed by the Session.
- """
- return Connection(self)
-
- @deprecated('[3.19] use a Connection object instead')
- def get_option_value(self, option, foreid=None):
- if foreid is not None:
- warn('[3.19] foreid argument is deprecated', DeprecationWarning,
- stacklevel=2)
- return self.repo.get_option_value(option)
-
- def _touch(self):
- """update latest session usage timestamp and reset mode to read"""
- self._timestamp.touch()
-
- local_perm_cache = cnx_attr('local_perm_cache')
-
- @local_perm_cache.setter
- def local_perm_cache(self, value):
- # base class assign an empty dict:-(
- assert value == {}
- pass
-
- # deprecated ###############################################################
-
- @property
- def anonymous_session(self):
- # XXX for now, anonymous_user only exists in webconfig (and testconfig).
- # It will only be present inside all-in-one instance.
- # there is plan to move it down to global config.
- if not hasattr(self.repo.config, 'anonymous_user'):
- # not a web or test config, no anonymous user
- return False
- return self.user.login == self.repo.config.anonymous_user()[0]
-
- @deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
- def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
- return getattr(self.rtype_eids_rdef(rtype, eidfrom, eidto), rprop)
-
- # 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
-
-
class InternalManager(object):
"""a manager user with all access rights used internally for task such as
bootstrapping the repository or creating regular users according to
@@ -1125,5 +934,4 @@
return None
-set_log_methods(Session, getLogger('cubicweb.session'))
set_log_methods(Connection, getLogger('cubicweb.session'))
--- a/cubicweb/server/sources/__init__.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/sources/__init__.py Mon Mar 20 10:28:01 2017 +0100
@@ -234,16 +234,6 @@
cnxset.cnx = self.get_connection()
cnxset.cu = cnxset.cnx.cursor()
- # cache handling ###########################################################
-
- def reset_caches(self):
- """method called during test to reset potential source caches"""
- pass
-
- def clear_eid_cache(self, eid, etype):
- """clear potential caches for the given eid"""
- pass
-
# user authentication api ##################################################
def authenticate(self, cnx, login, **kwargs):
--- a/cubicweb/server/sources/native.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/sources/native.py Mon Mar 20 10:28:01 2017 +0100
@@ -30,7 +30,7 @@
import sys
from six import PY2, text_type, string_types
-from six.moves import range, cPickle as pickle
+from six.moves import range, cPickle as pickle, zip
from logilab.common.decorators import cached, clear_cache
from logilab.common.configuration import Method
@@ -361,15 +361,17 @@
authentifier.source = self
authentifier.set_schema(self.schema)
- def reset_caches(self):
- """method called during test to reset potential source caches"""
- self._cache = QueryCache(self.repo.config['rql-cache-size'])
-
- def clear_eid_cache(self, eid, etype):
- """clear potential caches for the given eid"""
- self._cache.pop('Any X WHERE X eid %s, X is %s' % (eid, etype), None)
- self._cache.pop('Any X WHERE X eid %s' % eid, None)
- self._cache.pop('Any %s' % eid, None)
+ def clear_caches(self, eids, etypes):
+ """Clear potential source caches."""
+ if eids is None:
+ self._cache = QueryCache(self.repo.config['rql-cache-size'])
+ else:
+ cache = self._cache
+ for eid, etype in zip(eids, etypes):
+ cache.pop('Any X WHERE X eid %s' % eid, None)
+ cache.pop('Any %s' % eid, None)
+ if etype is not None:
+ cache.pop('Any X WHERE X eid %s, X is %s' % (eid, etype), None)
@statsd_timeit
def sqlexec(self, cnx, sql, args=None):
@@ -380,7 +382,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 +391,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"""
@@ -417,19 +419,13 @@
def restore(self, backupfile, confirm, drop, format='native'):
"""method called to restore a backup of source's data"""
- if self.repo.config.init_cnxset_pool:
- self.close_source_connections()
- try:
- if format == 'portable':
- helper = DatabaseIndependentBackupRestore(self)
- helper.restore(backupfile)
- elif format == 'native':
- self.restore_from_file(backupfile, confirm, drop=drop)
- else:
- raise ValueError('Unknown format %r' % format)
- finally:
- if self.repo.config.init_cnxset_pool:
- self.open_source_connections()
+ if format == 'portable':
+ helper = DatabaseIndependentBackupRestore(self)
+ helper.restore(backupfile)
+ elif format == 'native':
+ self.restore_from_file(backupfile, confirm, drop=drop)
+ else:
+ raise ValueError('Unknown format %r' % format)
def init(self, activated, source_entity):
super(NativeSQLSource, self).init(activated, source_entity)
--- a/cubicweb/server/sqlutils.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/sqlutils.py Mon Mar 20 10:28:01 2017 +0100
@@ -173,8 +173,8 @@
class ConnectionWrapper(object):
- """handle connection to the system source, at some point associated to a
- :class:`Session`
+ """Wrap a connection to the system source's database, attempting to handle
+ automatic reconnection.
"""
# since 3.19, we only have to manage the system source connection
--- a/cubicweb/server/test/data/hooks.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/data/hooks.py Mon Mar 20 10:28:01 2017 +0100
@@ -15,34 +15,23 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""
-"""
from cubicweb.server.hook import Hook
CALLED_EVENTS = {}
+
class StartupHook(Hook):
__regid__ = 'mystartup'
events = ('server_startup',)
+
def __call__(self):
CALLED_EVENTS['server_startup'] = True
+
class ShutdownHook(Hook):
__regid__ = 'myshutdown'
events = ('server_shutdown',)
+
def __call__(self):
CALLED_EVENTS['server_shutdown'] = True
-
-
-class LoginHook(Hook):
- __regid__ = 'mylogin'
- events = ('session_open',)
- def __call__(self):
- CALLED_EVENTS['session_open'] = self._cw.user.login
-
-class LogoutHook(Hook):
- __regid__ = 'mylogout'
- events = ('session_close',)
- def __call__(self):
- CALLED_EVENTS['session_close'] = self._cw.user.login
--- a/cubicweb/server/test/unittest_checkintegrity.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_checkintegrity.py Mon Mar 20 10:28:01 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):
--- a/cubicweb/server/test/unittest_hook.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_hook.py Mon Mar 20 10:28:01 2017 +0100
@@ -112,7 +112,7 @@
def test_call_hook(self):
self.o.register(AddAnyHook)
dis = set()
- cw = fake.FakeSession()
+ cw = fake.FakeConnection()
cw.is_hook_activated = lambda cls: cls.category not in dis
self.assertRaises(HookCalled,
self.o.call_hooks, 'before_add_entity', cw)
@@ -134,14 +134,6 @@
self.repo.hm.call_hooks('server_shutdown', repo=self.repo)
self.assertEqual(hooks.CALLED_EVENTS['server_shutdown'], True)
- def test_session_open_close(self):
- import hooks # cubicweb/server/test/data/hooks.py
- anonaccess = self.new_access('anon')
- with anonaccess.repo_cnx() as cnx:
- self.assertEqual(hooks.CALLED_EVENTS['session_open'], 'anon')
- anonaccess.close()
- self.assertEqual(hooks.CALLED_EVENTS['session_close'], 'anon')
-
if __name__ == '__main__':
unittest.main()
--- a/cubicweb/server/test/unittest_ldapsource.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_ldapsource.py Mon Mar 20 10:28:01 2017 +0100
@@ -246,9 +246,6 @@
self.assertRaises(AuthenticationError,
source.authenticate, cnx, 'syt', 'toto')
self.assertTrue(source.authenticate(cnx, 'syt', 'syt'))
- session = self.repo.new_session('syt', password='syt')
- self.assertTrue(session)
- session.close()
def test_base(self):
with self.admin_access.repo_cnx() as cnx:
@@ -280,7 +277,6 @@
eid = cnx.execute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
cnx.execute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': eid})
cnx.commit()
- source.reset_caches()
rset = cnx.execute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
self.assertEqual(len(rset), 1)
e = rset.get_entity(0, 0)
@@ -329,16 +325,18 @@
def test_a_filter_inactivate(self):
""" filtered out people should be deactivated, unable to authenticate """
+ repo_source = self.repo.sources_by_uri['ldap']
with self.admin_access.repo_cnx() as cnx:
source = cnx.execute('CWSource S WHERE S type="ldapfeed"').get_entity(0, 0)
- config = source.repo_source.check_config(source)
+ config = repo_source.check_config(source)
# filter with adim's phone number
config['user-filter'] = u'(%s=%s)' % ('telephoneNumber', '109')
- source.repo_source.update_config(source, config)
+ repo_source.update_config(source, config)
cnx.commit()
with self.repo.internal_cnx() as cnx:
self.pull(cnx)
- self.assertRaises(AuthenticationError, self.repo.new_session, 'syt', password='syt')
+ self.assertRaises(AuthenticationError,
+ repo_source.authenticate, cnx, 'syt', 'syt')
with self.admin_access.repo_cnx() as cnx:
self.assertEqual(cnx.execute('Any N WHERE U login "syt", '
'U in_state S, S name N').rows[0][0],
@@ -348,7 +346,7 @@
'activated')
# unfilter, syt should be activated again
config['user-filter'] = u''
- source.repo_source.update_config(source, config)
+ repo_source.update_config(source, config)
cnx.commit()
with self.repo.internal_cnx() as cnx:
self.pull(cnx)
@@ -367,7 +365,9 @@
self.delete_ldap_entry('uid=syt,ou=People,dc=cubicweb,dc=test')
with self.repo.internal_cnx() as cnx:
self.pull(cnx)
- self.assertRaises(AuthenticationError, self.repo.new_session, 'syt', password='syt')
+ source = self.repo.sources_by_uri['ldap']
+ self.assertRaises(AuthenticationError,
+ source.authenticate, cnx, 'syt', 'syt')
with self.admin_access.repo_cnx() as cnx:
self.assertEqual(cnx.execute('Any N WHERE U login "syt", '
'U in_state S, S name N').rows[0][0],
@@ -404,6 +404,7 @@
# test reactivating BY HAND the user isn't enough to
# authenticate, as the native source refuse to authenticate
# user from other sources
+ repo_source = self.repo.sources_by_uri['ldap']
self.delete_ldap_entry('uid=syt,ou=People,dc=cubicweb,dc=test')
with self.repo.internal_cnx() as cnx:
self.pull(cnx)
@@ -412,17 +413,16 @@
user = cnx.execute('CWUser U WHERE U login "syt"').get_entity(0, 0)
user.cw_adapt_to('IWorkflowable').fire_transition('activate')
cnx.commit()
- with self.assertRaises(AuthenticationError):
- self.repo.new_session('syt', password='syt')
+ self.assertRaises(AuthenticationError,
+ repo_source.authenticate, cnx, 'syt', 'syt')
# ok now let's try to make it a system user
cnx.execute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': user.eid})
cnx.commit()
- # and that we can now authenticate again
- self.assertRaises(AuthenticationError, self.repo.new_session, 'syt', password='toto')
- session = self.repo.new_session('syt', password='syt')
- self.assertTrue(session)
- session.close()
+ # and that we can now authenticate again
+ self.assertRaises(AuthenticationError,
+ repo_source.authenticate, cnx, 'syt', 'toto')
+ self.assertTrue(self.repo.authenticate_user(cnx, 'syt', password='syt'))
class LDAPFeedGroupTC(LDAPFeedTestBase):
--- a/cubicweb/server/test/unittest_migractions.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_migractions.py Mon Mar 20 10:28:01 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()
--- a/cubicweb/server/test/unittest_postgres.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_postgres.py Mon Mar 20 10:28:01 2017 +0100
@@ -52,7 +52,7 @@
def test_eid_range(self):
# concurrent allocation of eid ranges
- source = self.session.repo.sources_by_uri['system']
+ source = self.repo.sources_by_uri['system']
range1 = []
range2 = []
def allocate_eid_ranges(session, target):
--- a/cubicweb/server/test/unittest_querier.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_querier.py Mon Mar 20 10:28:01 2017 +0100
@@ -120,7 +120,7 @@
pass
def test_preprocess_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
reid = cnx.execute('Any X WHERE X is CWRType, X name "owned_by"')[0][0]
rqlst = self._prepare(cnx, 'Any COUNT(RDEF) WHERE RDEF relation_type X, X eid %(x)s',
{'x': reid})
@@ -128,7 +128,7 @@
rqlst.solutions)
def test_preprocess_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
teid = cnx.execute("INSERT Tag X: X name 'tag'")[0][0]
#geid = self.execute("CWGroup G WHERE G name 'users'")[0][0]
#self.execute("SET X tags Y WHERE X eid %(t)s, Y eid %(g)s",
@@ -144,8 +144,7 @@
text_type(parse(got)))
def test_preprocess_security(self):
- s = self.user_groups_session('users')
- with s.new_cnx() as cnx:
+ with self.user_groups_session('users') as cnx:
plan = self._prepare_plan(cnx, 'Any ETN,COUNT(X) GROUPBY ETN '
'WHERE X is ET, ET name ETN')
union = plan.rqlst
@@ -241,8 +240,7 @@
self.assertEqual(solutions, [{'X': 'Basket', 'ET': 'CWEType', 'ETN': 'String'}])
def test_preprocess_security_aggregat(self):
- s = self.user_groups_session('users')
- with s.new_cnx() as cnx:
+ with self.user_groups_session('users') as cnx:
plan = self._prepare_plan(cnx, 'Any MAX(X)')
union = plan.rqlst
plan.preprocess(union)
@@ -254,13 +252,13 @@
['MAX(X)'])
def test_preprocess_nonregr(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any S ORDERBY SI WHERE NOT S ecrit_par O, S para SI')
self.assertEqual(len(rqlst.solutions), 1)
def test_build_description(self):
# should return an empty result set
- rset = self.qexecute('Any X WHERE X eid %(x)s', {'x': self.session.user.eid})
+ rset = self.qexecute('Any X WHERE X eid %(x)s', {'x': self.admin_access._user.eid})
self.assertEqual(rset.description[0][0], 'CWUser')
rset = self.qexecute('Any 1')
self.assertEqual(rset.description[0][0], 'Int')
@@ -289,10 +287,9 @@
self.assertEqual(rset.description[0][0], 'String')
def test_build_descr1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rset = cnx.execute('(Any U,L WHERE U login L) UNION '
'(Any G,N WHERE G name N, G is CWGroup)')
- # rset.req = self.session
orig_length = len(rset)
rset.rows[0][0] = 9999999
description = manual_build_descr(cnx, rset.syntax_tree(), None, rset.rows)
@@ -493,7 +490,7 @@
rset = self.qexecute('DISTINCT Any G WHERE U? in_group G')
self.assertEqual(len(rset), 4)
rset = self.qexecute('DISTINCT Any G WHERE U? in_group G, U eid %(x)s',
- {'x': self.session.user.eid})
+ {'x': self.admin_access._user.eid})
self.assertEqual(len(rset), 4)
def test_select_ambigous_outer_join(self):
@@ -687,7 +684,7 @@
self.assertEqual(rset.rows[0][0], 12)
## def test_select_simplified(self):
-## ueid = self.session.user.eid
+## ueid = self.admin_access._user.eid
## rset = self.qexecute('Any L WHERE %s login L'%ueid)
## self.assertEqual(rset.rows[0][0], 'admin')
## rset = self.qexecute('Any L WHERE %(x)s login L', {'x':ueid})
@@ -840,7 +837,7 @@
def test_select_explicit_eid(self):
rset = self.qexecute('Any X,E WHERE X owned_by U, X eid E, U eid %(u)s',
- {'u': self.session.user.eid})
+ {'u': self.admin_access._user.eid})
self.assertTrue(rset)
self.assertEqual(rset.description[0][1], 'Int')
@@ -891,7 +888,7 @@
'Password', 'String',
'TZDatetime', 'TZTime',
'Time'])
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
cnx.create_entity('Personne', nom=u'louis', test=True)
self.assertEqual(len(cnx.execute('Any X WHERE X test %(val)s', {'val': True})), 1)
self.assertEqual(len(cnx.execute('Any X WHERE X test TRUE')), 1)
@@ -936,7 +933,7 @@
'(Any N,COUNT(X) GROUPBY N ORDERBY 2 WHERE X login N)')
def test_select_union_aggregat_independant_group(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
cnx.execute('INSERT State X: X name "hop"')
cnx.execute('INSERT State X: X name "hop"')
cnx.execute('INSERT Transition X: X name "hop"')
@@ -1149,7 +1146,7 @@
self.assertRaises(QueryError,
self.qexecute,
- "INSERT CWUser X: X login 'toto', X eid %s" % cnx.user(self.session).eid)
+ "INSERT CWUser X: X login 'toto', X eid %s" % cnx.user.eid)
def test_insertion_description_with_where(self):
rset = self.qexecute('INSERT CWUser E, EmailAddress EM: E login "X", E upassword "X", '
@@ -1185,8 +1182,7 @@
self.assertEqual(len(rset.rows), 0, rset.rows)
def test_delete_3(self):
- s = self.user_groups_session('users')
- with s.new_cnx() as cnx:
+ with self.user_groups_session('users') as cnx:
peid, = self.o.execute(cnx, "INSERT Personne P: P nom 'toto'")[0]
seid, = self.o.execute(cnx, "INSERT Societe S: S nom 'logilab'")[0]
self.o.execute(cnx, "SET P travaille S")
@@ -1224,7 +1220,7 @@
eeid, = self.qexecute('INSERT Email X: X messageid "<1234>", X subject "test", '
'X sender Y, X recipients Y WHERE Y is EmailAddress')[0]
self.qexecute("DELETE Email X")
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
sqlc = cnx.cnxset.cu
sqlc.execute('SELECT * FROM recipients_relation')
self.assertEqual(len(sqlc.fetchall()), 0)
@@ -1294,7 +1290,7 @@
self.assertEqual(self.qexecute('Any X WHERE X nom "tutu"').rows, [[peid2]])
def test_update_multiple2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
ueid = cnx.execute("INSERT CWUser X: X login 'bob', X upassword 'toto'")[0][0]
peid1 = cnx.execute("INSERT Personne Y: Y nom 'turlu'")[0][0]
peid2 = cnx.execute("INSERT Personne Y: Y nom 'tutu'")[0][0]
@@ -1342,7 +1338,7 @@
"WHERE X is Personne")
self.assertRaises(QueryError,
self.qexecute,
- "SET X login 'tutu', X eid %s" % cnx.user(self.session).eid)
+ "SET X login 'tutu', X eid %s" % cnx.user.eid)
# HAVING on write queries test #############################################
@@ -1375,7 +1371,7 @@
self.assertEqual(rset.description, [('CWUser',)])
self.assertRaises(Unauthorized,
self.qexecute, "Any P WHERE X is CWUser, X login 'bob', X upassword P")
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
cursor = cnx.cnxset.cu
cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
% (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
@@ -1387,7 +1383,7 @@
self.assertEqual(rset.description, [('CWUser',)])
def test_update_upassword(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rset = cnx.execute("INSERT CWUser X: X login 'bob', X upassword %(pwd)s",
{'pwd': 'toto'})
self.assertEqual(rset.description[0][0], 'CWUser')
@@ -1495,7 +1491,7 @@
'creation_date': '2000/07/03 11:00'})
rset = self.qexecute('Any lower(N) ORDERBY LOWER(N) WHERE X is Tag, X name N,'
'X owned_by U, U eid %(x)s',
- {'x':self.session.user.eid})
+ {'x':self.admin_access._user.eid})
self.assertEqual(rset.rows, [[u'\xe9name0']])
def test_nonregr_description(self):
@@ -1543,7 +1539,7 @@
self.qexecute('Any X ORDERBY D DESC WHERE X creation_date D')
def test_nonregr_extra_joins(self):
- ueid = self.session.user.eid
+ ueid = self.admin_access._user.eid
teid1 = self.qexecute("INSERT Folder X: X name 'folder1'")[0][0]
teid2 = self.qexecute("INSERT Folder X: X name 'folder2'")[0][0]
neid1 = self.qexecute("INSERT Note X: X para 'note1'")[0][0]
--- a/cubicweb/server/test/unittest_repository.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_repository.py Mon Mar 20 10:28:01 2017 +0100
@@ -77,21 +77,21 @@
self.assertFalse(cnx.execute('Any X WHERE NOT X cw_source S'))
def test_connect(self):
- session = self.repo.new_session(self.admlogin, password=self.admpassword)
- self.assertTrue(session.sessionid)
- session.close()
- self.assertRaises(AuthenticationError,
- self.repo.connect, self.admlogin, password='nimportnawak')
- self.assertRaises(AuthenticationError,
- self.repo.connect, self.admlogin, password='')
- self.assertRaises(AuthenticationError,
- self.repo.connect, self.admlogin, password=None)
- self.assertRaises(AuthenticationError,
- self.repo.connect, None, password=None)
- self.assertRaises(AuthenticationError,
- self.repo.connect, self.admlogin)
- self.assertRaises(AuthenticationError,
- self.repo.connect, None)
+ with self.repo.internal_cnx() as cnx:
+ self.assertTrue(
+ self.repo.authenticate_user(cnx, self.admlogin, password=self.admpassword))
+ self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+ cnx, self.admlogin, password='nimportnawak')
+ self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+ cnx, self.admlogin, password='')
+ self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+ cnx, self.admlogin, password=None)
+ self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+ cnx, None, password=None)
+ self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+ cnx, self.admlogin)
+ self.assertRaises(AuthenticationError, self.repo.authenticate_user,
+ cnx, None)
def test_login_upassword_accent(self):
with self.admin_access.repo_cnx() as cnx:
@@ -99,10 +99,8 @@
'X in_group G WHERE G name "users"',
{'login': u"barnabé", 'passwd': u"héhéhé".encode('UTF8')})
cnx.commit()
- repo = self.repo
- session = repo.new_session(u"barnabé", password=u"héhéhé".encode('UTF8'))
- self.assertTrue(session.sessionid)
- session.close()
+ repo = self.repo
+ self.assertTrue(repo.authenticate_user(cnx, u"barnabé", password=u"héhéhé".encode('UTF8')))
def test_rollback_on_execute_validation_error(self):
class ValidationErrorAfterHook(Hook):
@@ -142,14 +140,6 @@
cnx.rollback()
self.assertFalse(cnx.execute('Any X WHERE X is CWGroup, X name "toto"'))
-
- def test_close(self):
- repo = self.repo
- session = repo.new_session(self.admlogin, password=self.admpassword)
- self.assertTrue(session.sessionid)
- session.close()
-
-
def test_initial_schema(self):
schema = self.repo.schema
# check order of attributes is respected
@@ -193,13 +183,6 @@
ownedby = schema.rschema('owned_by')
self.assertEqual(ownedby.objects('CWEType'), ('CWUser',))
- def test_internal_api(self):
- repo = self.repo
- session = repo.new_session(self.admlogin, password=self.admpassword)
- with session.new_cnx() as cnx:
- self.assertEqual(repo.type_from_eid(2, cnx), 'CWGroup')
- session.close()
-
def test_public_api(self):
self.assertEqual(self.repo.get_schema(), self.repo.schema)
self.assertEqual(self.repo.source_defs(), {'system': {'type': 'native',
--- a/cubicweb/server/test/unittest_rqlannotation.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_rqlannotation.py Mon Mar 20 10:28:01 2017 +0100
@@ -40,7 +40,7 @@
pass
def test_0_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any SEN,RN,OEN WHERE X from_entity SE, '
'SE eid 44, X relation_type R, R eid 139, '
'X to_entity OE, OE eid 42, R name RN, SE name SEN, '
@@ -53,14 +53,14 @@
self.assertEqual(rqlst.defined_vars['R'].stinfo['attrvar'], None)
def test_0_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any O WHERE NOT S ecrit_par O, S eid 1, '
'S inline1 P, O inline2 P')
self.assertEqual(rqlst.defined_vars['P']._q_invariant, True)
self.assertEqual(rqlst.defined_vars['O'].stinfo['attrvar'], None)
def test_0_4(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any A,B,C WHERE A eid 12,A comment B, '
'A ?wf_info_for C')
self.assertEqual(rqlst.defined_vars['A']._q_invariant, False)
@@ -71,18 +71,18 @@
{'A': 'TrInfo', 'B': 'String', 'C': 'Note'}])
def test_0_5(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any P WHERE N ecrit_par P, N eid 0')
self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['P']._q_invariant, True)
def test_0_6(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any P WHERE NOT N ecrit_par P, N eid 512')
self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
def test_0_7(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Personne X,Y where X nom NX, '
'Y nom NX, X eid XE, not Y eid XE')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
@@ -90,25 +90,25 @@
self.assertTrue(rqlst.defined_vars['XE'].stinfo['attrvar'])
def test_0_8(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any P WHERE X eid 0, NOT X connait P')
self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
self.assertEqual(len(rqlst.solutions), 1, rqlst.solutions)
def test_0_10(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X concerne Y, Y is Note')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
def test_0_11(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X todo_by Y, X is Affaire')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
def test_0_12(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Personne P WHERE P concerne A, '
'A concerne S, S nom "Logilab"')
self.assertEqual(rqlst.defined_vars['P']._q_invariant, True)
@@ -116,31 +116,31 @@
self.assertEqual(rqlst.defined_vars['S']._q_invariant, False)
def test_1_0(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X,Y WHERE X created_by Y, '
'X eid 5, NOT Y eid 6')
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
def test_1_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X,Y WHERE X created_by Y, X eid 5, '
'NOT Y eid IN (6,7)')
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
def test_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X identity Y, Y eid 1')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
def test_7(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Personne X,Y where X nom NX, Y nom NX, '
'X eid XE, not Y eid XE')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
def test_8(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
# DISTINCT Any P WHERE P require_group %(g)s,
# NOT %(u)s has_group_permission P, P is CWPermission
rqlst = self._prepare(cnx, 'DISTINCT Any X WHERE A concerne X, '
@@ -149,69 +149,69 @@
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
def test_diff_scope_identity_deamb(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X concerne Y, Y is Note, '
'EXISTS(Y identity Z, Z migrated_from N)')
self.assertEqual(rqlst.defined_vars['Z']._q_invariant, True)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
def test_optional_inlined(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X,S where X from_state S?')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['S']._q_invariant, True)
def test_optional_inlined_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any N,A WHERE N? inline1 A')
self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['A']._q_invariant, False)
def test_optional_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X,S WHERE X travaille S?')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['S']._q_invariant, True)
def test_greater_eid(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X eid > 5')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
def test_greater_eid_typed(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X eid > 5, X is Note')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
def test_max_eid(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any MAX(X)')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
def test_max_eid_typed(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any MAX(X) WHERE X is Note')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
def test_all_entities(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
def test_all_typed_entity(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X is Note')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
def test_has_text_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X has_text "toto tata"')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
self.assertEqual(rqlst.defined_vars['X'].stinfo['principal'].r_type,
'has_text')
def test_has_text_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X is Personne, '
'X has_text "coucou"')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
@@ -219,7 +219,7 @@
'has_text')
def test_not_relation_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
# P can't be invariant since deambiguification caused by "NOT X require_permission P"
# is not considered by generated sql (NOT EXISTS(...))
rqlst = self._prepare(cnx, 'Any P,G WHERE P require_group G, '
@@ -229,134 +229,134 @@
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
def test_not_relation_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'TrInfo X WHERE X eid 2, '
'NOT X from_state Y, Y is State')
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
def test_not_relation_3(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X, Y WHERE X eid 1, Y eid in (2, 3)')
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
def test_not_relation_4_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Note X WHERE NOT Y evaluee X')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
def test_not_relation_4_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE NOT Y evaluee X')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
def test_not_relation_4_3(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any Y WHERE NOT Y evaluee X')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
def test_not_relation_4_4(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE NOT Y evaluee X, Y is CWUser')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
def test_not_relation_4_5(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE NOT Y evaluee X, '
'Y eid %s, X is Note' % self.ueid)
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.solutions, [{'X': 'Note'}])
def test_not_relation_5_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X,Y WHERE X name "CWGroup", '
'Y eid IN(1, 2, 3), NOT X read_permission Y')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
def test_not_relation_5_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'DISTINCT Any X,Y WHERE X name "CWGroup", '
'Y eid IN(1, 2, 3), NOT X read_permission Y')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
def test_not_relation_6(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Personne P where NOT P concerne A')
self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['A']._q_invariant, True)
def test_not_relation_7(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any K,V WHERE P is CWProperty, '
'P pkey K, P value V, NOT P for_user U')
self.assertEqual(rqlst.defined_vars['P']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['U']._q_invariant, True)
def test_exists_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any U WHERE U eid IN (1,2), EXISTS(X owned_by U)')
self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
def test_exists_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any U WHERE EXISTS(U eid IN (1,2), X owned_by U)')
self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
def test_exists_3(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any U WHERE EXISTS(X owned_by U, X bookmarked_by U)')
self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
def test_exists_4(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X,Y WHERE X name "CWGroup", '
'Y eid IN(1, 2, 3), EXISTS(X read_permission Y)')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
def test_exists_5(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'DISTINCT Any X,Y WHERE X name "CWGroup", '
'Y eid IN(1, 2, 3), EXISTS(X read_permission Y)')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
def test_not_exists_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any U WHERE NOT EXISTS(X owned_by U, '
'X bookmarked_by U)')
self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
def test_not_exists_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X,Y WHERE X name "CWGroup", '
'Y eid IN(1, 2, 3), NOT EXISTS(X read_permission Y)')
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
def test_not_exists_distinct_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'DISTINCT Any X,Y WHERE X name "CWGroup", '
'Y eid IN(1, 2, 3), NOT EXISTS(X read_permission Y)')
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, False)
def test_or_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X concerne B OR '
'C concerne X, B eid 12, C eid 13')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
def test_or_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X created_by U, X concerne B OR '
'C concerne X, B eid 12, C eid 13')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
@@ -364,14 +364,14 @@
self.assertEqual(rqlst.defined_vars['X'].stinfo['principal'].r_type, 'created_by')
def test_or_3(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any N WHERE A evaluee N or EXISTS(N todo_by U)')
self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['A']._q_invariant, True)
self.assertEqual(rqlst.defined_vars['U']._q_invariant, True)
def test_or_exists_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
# query generated by security rewriting
rqlst = self._prepare(cnx, 'DISTINCT Any A,S WHERE A is Affaire, S nom "chouette", '
'S is IN(Division, Societe, SubDivision),'
@@ -388,7 +388,7 @@
self.assertEqual(rqlst.defined_vars['S']._q_invariant, False)
def test_or_exists_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any U WHERE EXISTS(U in_group G, G name "managers") OR '
'EXISTS(X owned_by U, X bookmarked_by U)')
self.assertEqual(rqlst.defined_vars['U']._q_invariant, False)
@@ -396,7 +396,7 @@
self.assertEqual(rqlst.defined_vars['X']._q_invariant, True)
def test_or_exists_3(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any COUNT(S),CS GROUPBY CS ORDERBY 1 DESC LIMIT 10 '
'WHERE C is Societe, S concerne C, C nom CS, '
'(EXISTS(S owned_by D)) '
@@ -409,14 +409,14 @@
self.assertEqual(rqlst.defined_vars['S']._q_invariant, True)
def test_nonregr_ambiguity(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Note N WHERE N attachment F')
# N may be an image as well, not invariant
self.assertEqual(rqlst.defined_vars['N']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['F']._q_invariant, True)
def test_nonregr_ambiguity_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any S,SN WHERE X has_text "tot", '
'X in_state S, S name SN, X is CWUser')
# X use has_text but should not be invariant as ambiguous, and has_text
@@ -425,19 +425,19 @@
self.assertEqual(rqlst.defined_vars['S']._q_invariant, False)
def test_remove_from_deleted_source_1(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Note X WHERE X eid 999998, NOT X cw_source Y')
self.assertNotIn('X', rqlst.defined_vars) # simplified
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
def test_remove_from_deleted_source_2(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Note X WHERE X eid IN (999998, 999999), NOT X cw_source Y')
self.assertEqual(rqlst.defined_vars['X']._q_invariant, False)
self.assertEqual(rqlst.defined_vars['Y']._q_invariant, True)
def test_has_text_security_cache_bug(self):
- with self.session.new_cnx() as cnx:
+ with self.admin_access.cnx() as cnx:
rqlst = self._prepare(cnx, 'Any X WHERE X has_text "toto" WITH X BEING '
'(Any C WHERE C is Societe, C nom CS)')
self.assertTrue(rqlst.parent.has_text_query)
--- a/cubicweb/server/test/unittest_security.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_security.py Mon Mar 20 10:28:01 2017 +0100
@@ -85,15 +85,13 @@
oldhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser "
"WHERE cw_login = 'oldpassword'").fetchone()[0]
oldhash = self.repo.system_source.binary_to_str(oldhash)
- session = self.repo.new_session('oldpassword', password='oldpassword')
- session.close()
+ self.repo.authenticate_user(cnx, 'oldpassword', password='oldpassword')
newhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser "
"WHERE cw_login = 'oldpassword'").fetchone()[0]
newhash = self.repo.system_source.binary_to_str(newhash)
self.assertNotEqual(oldhash, newhash)
self.assertTrue(newhash.startswith(b'$6$'))
- session = self.repo.new_session('oldpassword', password='oldpassword')
- session.close()
+ self.repo.authenticate_user(cnx, 'oldpassword', password='oldpassword')
newnewhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE "
"cw_login = 'oldpassword'").fetchone()[0]
newnewhash = self.repo.system_source.binary_to_str(newnewhash)
@@ -305,8 +303,8 @@
cnx.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
{'x': ueid, 'passwd': b'newpwd'})
cnx.commit()
- session = self.repo.new_session('user', password='newpwd')
- session.close()
+ with self.repo.internal_cnx() as cnx:
+ self.repo.authenticate_user(cnx, 'user', password='newpwd')
def test_user_cant_change_other_upassword(self):
with self.admin_access.repo_cnx() as cnx:
@@ -523,9 +521,9 @@
with self.temporary_permissions(Division={'read': ('managers',
ERQLExpression('X owned_by U'))}):
with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+ rqlst = self.repo.vreg.rqlhelper.parse('Any X WHERE X is_instance_of Societe')
+ self.repo.vreg.solutions(cnx, rqlst, {})
querier = cnx.repo.querier
- rqlst = querier.parse('Any X WHERE X is_instance_of Societe')
- querier.solutions(cnx, rqlst, {})
querier._annotate(rqlst)
plan = querier.plan_factory(rqlst, {}, cnx)
plan.preprocess(rqlst)
--- a/cubicweb/server/test/unittest_serverctl.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_serverctl.py Mon Mar 20 10:28:01 2017 +0100
@@ -1,16 +1,27 @@
import os.path as osp
import shutil
+from mock import patch
+
from cubicweb import ExecutionError
from cubicweb.devtools import testlib, ApptestConfiguration
-from cubicweb.server.serverctl import _local_dump, DBDumpCommand, SynchronizeSourceCommand
+from cubicweb.server.serverctl import (
+ DBDumpCommand,
+ RepositorySchedulerCommand,
+ SynchronizeSourceCommand,
+)
from cubicweb.server.serverconfig import ServerConfiguration
+
class ServerCTLTC(testlib.CubicWebTC):
+
def setUp(self):
super(ServerCTLTC, self).setUp()
self.orig_config_for = ServerConfiguration.config_for
- config_for = lambda appid: ApptestConfiguration(appid, __file__)
+
+ def config_for(appid):
+ return ApptestConfiguration(appid, __file__)
+
ServerConfiguration.config_for = staticmethod(config_for)
def tearDown(self):
@@ -21,6 +32,26 @@
DBDumpCommand(None).run([self.appid])
shutil.rmtree(osp.join(self.config.apphome, 'backup'))
+ def test_scheduler(self):
+ cmd = RepositorySchedulerCommand(None)
+ with patch('sched.scheduler.run',
+ side_effect=RuntimeError('boom')) as patched_run:
+ with self.assertRaises(RuntimeError) as exc_cm:
+ with self.assertLogs('cubicweb.repository', level='INFO') as log_cm:
+ cmd.run([self.appid])
+ # make sure repository scheduler started
+ scheduler_start_message = (
+ 'INFO:cubicweb.repository:starting repository scheduler with '
+ 'tasks: update_feeds, expire_dataimports'
+ )
+ self.assertIn(scheduler_start_message, log_cm.output)
+ # and that scheduler's run method got called
+ self.assertIn('boom', str(exc_cm.exception))
+ patched_run.assert_called_once_with()
+ # make sure repository's shutdown method got called
+ repo_shutdown_message = 'INFO:cubicweb.repository:shutting down repository'
+ self.assertIn(repo_shutdown_message, log_cm.output)
+
def test_source_sync(self):
with self.admin_access.repo_cnx() as cnx:
cnx.create_entity('CWSource', name=u'success_feed', type=u'datafeed',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/unittest_session.py Mon Mar 20 10:28:01 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 <http://www.gnu.org/licenses/>.
+
+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()
--- a/cubicweb/server/test/unittest_utils.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/test/unittest_utils.py Mon Mar 20 10:28:01 2017 +0100
@@ -15,29 +15,54 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""
+"""Tests for cubicweb.server.utils module."""
-"""
-from logilab.common.testlib import TestCase, unittest_main
-
+from cubicweb.devtools import testlib
from cubicweb.server import utils
-class UtilsTC(TestCase):
+
+class UtilsTC(testlib.BaseTestCase):
+
def test_crypt(self):
for hash in (
- utils.crypt_password('xxx'), # default sha512
- b'ab$5UsKFxRKKN.d8iBIFBnQ80', # custom md5
- b'ab4Vlm81ZUHlg', # DES
- ):
+ utils.crypt_password('xxx'), # default sha512
+ b'ab$5UsKFxRKKN.d8iBIFBnQ80', # custom md5
+ b'ab4Vlm81ZUHlg', # DES
+ ):
self.assertEqual(utils.crypt_password('xxx', hash), hash)
self.assertEqual(utils.crypt_password(u'xxx', hash), hash)
- self.assertEqual(utils.crypt_password(u'xxx', hash.decode('ascii')), hash.decode('ascii'))
+ self.assertEqual(utils.crypt_password(u'xxx', hash.decode('ascii')),
+ hash.decode('ascii'))
self.assertEqual(utils.crypt_password('yyy', hash), b'')
# accept any password for empty hashes (is it a good idea?)
self.assertEqual(utils.crypt_password('xxx', ''), '')
self.assertEqual(utils.crypt_password('yyy', ''), '')
+ def test_schedule_periodic_task(self):
+ scheduler = utils.scheduler()
+ this = []
+
+ def fill_this(x):
+ this.append(x)
+ if len(this) > 2:
+ raise SystemExit()
+ elif len(this) > 1:
+ raise RuntimeError()
+
+ event = utils.schedule_periodic_task(scheduler, 0.01, fill_this, 1)
+ self.assertEqual(event.action.__name__, 'fill_this')
+ self.assertEqual(len(scheduler.queue), 1)
+
+ with self.assertLogs('cubicweb.scheduler', level='ERROR') as cm:
+ scheduler.run()
+ self.assertEqual(this, [1] * 3)
+ self.assertEqual(len(cm.output), 2)
+ self.assertIn('Unhandled exception in periodic task "fill_this"',
+ cm.output[0])
+ self.assertIn('"fill_this" not re-scheduled', cm.output[1])
+
if __name__ == '__main__':
- unittest_main()
+ import unittest
+ unittest.main()
--- a/cubicweb/server/utils.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/server/utils.py Mon Mar 20 10:28:01 2017 +0100
@@ -19,13 +19,14 @@
from __future__ import print_function
-
+from functools import wraps
+import sched
import sys
import logging
-from threading import Timer, Thread
+from threading import Thread
from getpass import getpass
-from six import PY2, text_type
+from six import PY2
from six.moves import input
from passlib.utils import handlers as uh, to_hash_str
@@ -58,26 +59,29 @@
return md5crypt(secret, self.salt.encode('ascii')).decode('utf-8')
_calc_checksum = calc_checksum
+
_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'],
deprecated=['cubicwebmd5crypt', 'des_crypt'])
verify_and_update = _CRYPTO_CTX.verify_and_update
+
def crypt_password(passwd, salt=None):
"""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
try:
if _CRYPTO_CTX.verify(passwd, salt):
return salt
- except ValueError: # e.g. couldn't identify hash
+ except ValueError: # e.g. couldn't identify hash
pass
# wrong password
return b''
+
@deprecated('[3.22] no more necessary, directly get eschema.eid')
def eschema_eid(cnx, eschema):
"""get eid of the CWEType entity for the given yams type.
@@ -92,6 +96,7 @@
DEFAULT_MSG = 'we need a manager connection on the repository \
(the server doesn\'t have to run, even should better not)'
+
def manager_userpasswd(user=None, msg=DEFAULT_MSG, confirm=False,
passwdmsg='password'):
if not user:
@@ -113,7 +118,51 @@
return user, passwd
+if PY2:
+ import time # noqa
+
+ class scheduler(sched.scheduler):
+ """Python2 version of sched.scheduler that matches Python3 API."""
+
+ def __init__(self, **kwargs):
+ kwargs.setdefault('timefunc', time.time)
+ kwargs.setdefault('delayfunc', time.sleep)
+ # sched.scheduler is an old-style class.
+ sched.scheduler.__init__(self, **kwargs)
+
+else:
+ scheduler = sched.scheduler
+
+
+def schedule_periodic_task(scheduler, interval, func, *args):
+ """Enter a task with `func(*args)` as a periodic event in `scheduler`
+ executing at `interval` seconds. Once executed, the task would re-schedule
+ itself unless a BaseException got raised.
+ """
+ @wraps(func)
+ def task(*args):
+ restart = True
+ try:
+ func(*args)
+ except Exception:
+ logger = logging.getLogger('cubicweb.scheduler')
+ logger.exception('Unhandled exception in periodic task "%s"',
+ func.__name__)
+ except BaseException as exc:
+ logger = logging.getLogger('cubicweb.scheduler')
+ logger.error('periodic task "%s" not re-scheduled due to %r',
+ func.__name__, exc)
+ restart = False
+ finally:
+ if restart:
+ scheduler.enter(interval, 1, task, argument=args)
+
+ return scheduler.enter(interval, 1, task, argument=args)
+
+
_MARKER = object()
+
+
def func_name(func):
name = getattr(func, '__name__', _MARKER)
if name is _MARKER:
@@ -122,46 +171,6 @@
name = repr(func)
return name
-class LoopTask(object):
- """threaded task restarting itself once executed"""
- def __init__(self, tasks_manager, interval, func, args):
- if interval < 0:
- raise ValueError('Loop task interval must be >= 0 '
- '(current value: %f for %s)' % \
- (interval, func_name(func)))
- self._tasks_manager = tasks_manager
- self.interval = interval
- def auto_restart_func(self=self, func=func, args=args):
- restart = True
- try:
- func(*args)
- except Exception:
- logger = logging.getLogger('cubicweb.repository')
- logger.exception('Unhandled exception in LoopTask %s', self.name)
- raise
- except BaseException:
- restart = False
- finally:
- if restart and tasks_manager.running:
- self.start()
- self.func = auto_restart_func
- self.name = func_name(func)
-
- def __str__(self):
- return '%s (%s seconds)' % (self.name, self.interval)
-
- def start(self):
- self._t = Timer(self.interval, self.func)
- self._t.setName('%s-%s[%d]' % (self._t.getName(), self.name, self.interval))
- self._t.start()
-
- def cancel(self):
- self._t.cancel()
-
- def join(self):
- if self._t.isAlive():
- self._t.join()
-
class RepoThread(Thread):
"""subclass of thread so it auto remove itself from a given list once
@@ -188,56 +197,3 @@
def getName(self):
return '%s(%s)' % (self._name, Thread.getName(self))
-
-class TasksManager(object):
- """Object dedicated manage background task"""
-
- def __init__(self):
- self.running = False
- self._tasks = []
- self._looping_tasks = []
-
- def add_looping_task(self, interval, func, *args):
- """register a function to be called every `interval` seconds.
-
- If interval is negative, no looping task is registered.
- """
- if interval < 0:
- self.debug('looping task %s ignored due to interval %f < 0',
- func_name(func), interval)
- return
- task = LoopTask(self, interval, func, args)
- if self.running:
- self._start_task(task)
- else:
- self._tasks.append(task)
-
- def _start_task(self, task):
- self._looping_tasks.append(task)
- self.info('starting task %s with interval %.2fs', task.name,
- task.interval)
- task.start()
-
- def start(self):
- """Start running looping task"""
- assert self.running == False # bw compat purpose maintly
- while self._tasks:
- task = self._tasks.pop()
- self._start_task(task)
- self.running = True
-
- def stop(self):
- """Stop all running task.
-
- returns when all task have been cancel and none are running anymore"""
- if self.running:
- while self._looping_tasks:
- looptask = self._looping_tasks.pop()
- self.info('canceling task %s...', looptask.name)
- looptask.cancel()
- looptask.join()
- self.info('task %s finished', looptask.name)
-
-from logging import getLogger
-from cubicweb import set_log_methods
-set_log_methods(TasksManager, getLogger('cubicweb.repository'))
--- a/cubicweb/skeleton/DISTNAME.spec.tmpl Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/skeleton/DISTNAME.spec.tmpl Mon Mar 20 10:28:01 2017 +0100
@@ -35,13 +35,10 @@
%%install
%%{__python} setup.py --quiet install --no-compile --prefix=%%{_prefix} --root="$RPM_BUILD_ROOT"
-# remove generated .egg-info file
-rm -rf $RPM_BUILD_ROOT/usr/lib/python*
-
%%clean
rm -rf $RPM_BUILD_ROOT
%%files
%%defattr(-, root, root)
-%%{_prefix}/share/cubicweb/cubes/*
+%%{_python_sitelib}/*
--- a/cubicweb/skeleton/MANIFEST.in.tmpl Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/skeleton/MANIFEST.in.tmpl Mon Mar 20 10:28:01 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
--- a/cubicweb/skeleton/cubicweb_CUBENAME/__init__.py.tmpl Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/skeleton/cubicweb_CUBENAME/__init__.py.tmpl Mon Mar 20 10:28:01 2017 +0100
@@ -2,3 +2,7 @@
%(longdesc)s
"""
+
+
+def includeme(config):
+ pass
--- a/cubicweb/sobjects/notification.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/sobjects/notification.py Mon Mar 20 10:28:01 2017 +0100
@@ -25,14 +25,12 @@
from six import text_type
from logilab.common.textutils import normalize_text
-from logilab.common.deprecation import class_renamed, class_moved, deprecated
from logilab.common.registry import yes
-from cubicweb.entity import Entity
from cubicweb.view import Component, EntityView
from cubicweb.server.hook import SendMailOp
from cubicweb.mail import construct_message_id, format_mail
-from cubicweb.server.session import Session, InternalManager
+from cubicweb.server.session import Connection, InternalManager
class RecipientsFinder(Component):
@@ -54,7 +52,7 @@
elif mode == 'default-dest-addrs':
lang = self._cw.vreg.property_value('ui.language')
dests = zip(self._cw.vreg.config['default-dest-addrs'], repeat(lang))
- else: # mode == 'none'
+ else: # mode == 'none'
dests = []
return dests
@@ -75,8 +73,8 @@
msgid_timestamp = True
# to be defined on concrete sub-classes
- content = None # body of the mail
- message = None # action verb of the subject
+ content = None # body of the mail
+ message = None # action verb of the subject
# this is usually the method to call
def render_and_send(self, **kwargs):
@@ -120,8 +118,7 @@
emailaddr = something.cw_adapt_to('IEmailable').get_email()
user = something
# hi-jack self._cw to get a session for the returned user
- session = Session(user, self._cw.repo)
- with session.new_cnx() as cnx:
+ with Connection(self._cw.repo, user) as cnx:
self._cw = cnx
try:
# since the same view (eg self) may be called multiple time and we
@@ -169,7 +166,7 @@
def format_section(self, attr, value):
return '%(attr)s\n%(ul)s\n%(value)s\n' % {
- 'attr': attr, 'ul': '-'*len(attr), 'value': value}
+ 'attr': attr, 'ul': '-' * len(attr), 'value': value}
def subject(self):
entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
@@ -183,12 +180,12 @@
entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
for key, val in kwargs.items():
if val and isinstance(val, text_type) and val.strip():
- kwargs[key] = self._cw._(val)
+ kwargs[key] = self._cw._(val)
kwargs.update({'user': self.user_data['login'],
'eid': entity.eid,
'etype': entity.dc_type(),
- 'url': entity.absolute_url(__secure__=True),
- 'title': entity.dc_long_title(),})
+ 'url': entity.absolute_url(),
+ 'title': entity.dc_long_title()})
return kwargs
@@ -250,8 +247,8 @@
def subject(self):
entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
- return u'%s #%s (%s)' % (self._cw.__('New %s' % entity.e_schema),
- entity.eid, self.user_data['login'])
+ return u'%s #%s (%s)' % (self._cw.__('New %s' % entity.e_schema),
+ entity.eid, self.user_data['login'])
def format_value(value):
@@ -316,5 +313,5 @@
def subject(self):
entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
- return u'%s #%s (%s)' % (self._cw.__('Updated %s' % entity.e_schema),
- entity.eid, self.user_data['login'])
+ return u'%s #%s (%s)' % (self._cw.__('Updated %s' % entity.e_schema),
+ entity.eid, self.user_data['login'])
--- a/cubicweb/sobjects/services.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/sobjects/services.py Mon Mar 20 10:28:01 2017 +0100
@@ -30,31 +30,28 @@
resources usage.
"""
- __regid__ = 'repo_stats'
+ __regid__ = 'repo_stats'
__select__ = match_user_groups('managers', 'users')
def call(self):
- repo = self._cw.repo # Service are repo side only.
+ repo = self._cw.repo # Service are repo side only.
results = {}
querier = repo.querier
source = repo.system_source
for size, maxsize, hits, misses, title in (
- (len(querier._rql_cache), repo.config['rql-cache-size'],
- querier.cache_hit, querier.cache_miss, 'rqlt_st'),
+ (len(querier.rql_cache), repo.config['rql-cache-size'],
+ querier.rql_cache.cache_hit, querier.rql_cache.cache_miss, 'rqlt_st'),
(len(source._cache), repo.config['rql-cache-size'],
- source.cache_hit, source.cache_miss, 'sql'),
- ):
+ source.cache_hit, source.cache_miss, 'sql'),
+ ):
results['%s_cache_size' % title] = {'size': size, 'maxsize': maxsize}
results['%s_cache_hit' % title] = hits
results['%s_cache_miss' % title] = misses
results['%s_cache_hit_percent' % title] = (hits * 100) / (hits + misses)
results['type_cache_size'] = len(repo._type_cache)
results['sql_no_cache'] = repo.system_source.no_cache
- results['nb_open_sessions'] = len(repo._sessions)
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
@@ -64,15 +61,13 @@
resources usage.
"""
- __regid__ = 'repo_gc_stats'
+ __regid__ = 'repo_gc_stats'
__select__ = match_user_groups('managers')
def call(self, nmax=20):
"""Return a dictionary containing some statistics about the repository
memory usage.
- This is a public method, not requiring a session id.
-
nmax is the max number of (most) referenced object returned as
the 'referenced' result
"""
@@ -86,11 +81,6 @@
lookupclasses = (AppObject,
Union, ResultSet,
CubicWebRequestBase)
- try:
- from cubicweb.server.session import Session, InternalSession
- lookupclasses += (InternalSession, Session)
- except ImportError:
- pass # no server part installed
results = {}
counters, ocounters, garbage = gc_info(lookupclasses,
--- a/cubicweb/test/data/views.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/data/views.py Mon Mar 20 10:28:01 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 <http://www.gnu.org/licenses/>.
-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):
--- a/cubicweb/test/data_schemareader/schema.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/data_schemareader/schema.py Mon Mar 20 10:28:01 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',),
--- a/cubicweb/test/unittest_cubes.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_cubes.py Mon Mar 20 10:28:01 2017 +0100
@@ -20,36 +20,36 @@
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, TestCase
@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)
+ check_code = ("import logging\n"
+ "logging.getLogger('cubicweb_foo')"
+ ".warn('imported %s', __name__)\n")
+ with open(path.join(cubedir, '__init__.py'), 'w') as f:
+ f.write("'cubicweb_foo application package'\n" + check_code)
+ with open(path.join(cubedir, 'bar.py'), 'w') as f:
+ f.write(check_code + 'baz = 1\n')
+ sys.path.append(libdir)
+ yield cubedir
+ finally:
+ sys.path.remove(libdir)
-class CubesImporterTC(unittest.TestCase):
+class CubesImporterTC(TestCase):
def setUp(self):
# During discovery, CubicWebConfiguration.cls_adjust_sys_path may be
@@ -95,6 +95,38 @@
from cubes.foo import bar
self.assertEqual(bar.baz, 1)
+ def test_reload_cube(self):
+ """reloading cubes twice should return the same module"""
+ CubicWebConfiguration.cls_adjust_sys_path()
+ import cubes
+ if PY2:
+ new = reload(cubes)
+ else:
+ import importlib
+ new = importlib.reload(cubes)
+ self.assertIs(new, cubes)
+
+ def test_no_double_import(self):
+ """Check new and legacy import the same module once"""
+ with temp_cube():
+ CubicWebConfiguration.cls_adjust_sys_path()
+ with self.assertLogs('cubicweb_foo', 'WARNING') as cm:
+ from cubes.foo import bar
+ from cubicweb_foo import bar as bar2
+ self.assertIs(bar, bar2)
+ self.assertIs(sys.modules['cubes.foo'],
+ sys.modules['cubicweb_foo'])
+ self.assertEqual(cm.output, [
+ 'WARNING:cubicweb_foo:imported cubicweb_foo',
+ # module __name__ for subpackage differ along python version
+ # for PY2 it's based on how the module was imported "from
+ # cubes.foo import bar" and for PY3 based on __name__ of parent
+ # module "cubicweb_foo". Not sure if it's an issue, but PY3
+ # behavior looks better.
+ 'WARNING:cubicweb_foo:imported ' + (
+ 'cubes.foo.bar' if PY2 else 'cubicweb_foo.bar')
+ ])
+
def test_import_legacy_cube(self):
"""Check that importing a legacy cube works when sys.path got adjusted.
"""
--- a/cubicweb/test/unittest_cwconfig.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_cwconfig.py Mon Mar 20 10:28:01 2017 +0100
@@ -17,8 +17,12 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""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
@@ -78,9 +127,9 @@
@patch('pkg_resources.iter_entry_points', side_effect=iter_entry_points)
def test_available_cubes(self, mock_iter_entry_points):
expected_cubes = [
- 'card', 'comment', 'cubicweb_comment', 'cubicweb_email', 'file',
- 'cubicweb_file', 'cubicweb_forge', 'localperms',
- 'cubicweb_mycube', 'tag',
+ 'card', 'comment', 'email', 'file',
+ 'forge', 'localperms',
+ 'mycube', 'tag',
]
self._test_available_cubes(expected_cubes)
mock_iter_entry_points.assert_called_once_with(
@@ -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()
--- a/cubicweb/test/unittest_cwctl.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_cwctl.py Mon Mar 20 10:28:01 2017 +0100
@@ -24,11 +24,10 @@
from six import PY2
from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.cwctl import ListCommand
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.server.migractions import ServerMigrationHelper
-CubicWebConfiguration.load_cwctl_plugins() # XXX necessary?
-
class CubicWebCtlTC(unittest.TestCase):
@@ -40,9 +39,15 @@
sys.stdout = sys.__stdout__
def test_list(self):
- from cubicweb.cwctl import ListCommand
ListCommand(None).run([])
+ def test_list_configurations(self):
+ ListCommand(None).run(['configurations'])
+ configs = [l[2:].strip() for l in self.stream.getvalue().splitlines()
+ if l.startswith('* ')]
+ self.assertIn('all-in-one', configs)
+ self.assertIn('pyramid', configs)
+
class CubicWebShellTC(CubicWebTC):
--- a/cubicweb/test/unittest_entity.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_entity.py Mon Mar 20 10:28:01 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:
--- a/cubicweb/test/unittest_repoapi.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_repoapi.py Mon Mar 20 10:28:01 2017 +0100
@@ -56,7 +56,7 @@
"""Check that ClientConnection requires explicit open and close
"""
access = self.admin_access
- cltcnx = Connection(access._session)
+ cltcnx = Connection(access._repo, access._user)
# connection not open yet
with self.assertRaises(ProgrammingError):
cltcnx.execute('Any X WHERE X is CWUser')
--- a/cubicweb/test/unittest_req.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_req.py Mon Mar 20 10:28:01 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:
--- a/cubicweb/test/unittest_rqlrewrite.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_rqlrewrite.py Mon Mar 20 10:28:01 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')
@@ -506,9 +512,9 @@
if args is None:
args = {}
querier = self.repo.querier
- union = querier.parse(rql)
+ union = parse(rql) # self.vreg.parse(rql, annotate=True)
with self.admin_access.repo_cnx() as cnx:
- querier.solutions(cnx, union, args)
+ self.vreg.solutions(cnx, union, args)
querier._annotate(union)
plan = querier.plan_factory(union, args, cnx)
plan.preprocess(union)
--- a/cubicweb/test/unittest_rset.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_rset.py Mon Mar 20 10:28:01 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))
@@ -88,7 +89,7 @@
self.rset = ResultSet([[12, 'adim'], [13, 'syt']],
'Any U,L where U is CWUser, U login L',
description=[['CWUser', 'String'], ['Bar', 'String']])
- self.rset.req = mock_object(vreg=self.vreg)
+ self.rset.req = mock_object(vreg=self.vreg, repo=self.repo)
def compare_urls(self, url1, url2):
info1 = urlsplit(url1)
@@ -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')
--- a/cubicweb/test/unittest_rtags.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_rtags.py Mon Mar 20 10:28:01 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 <http://www.gnu.org/licenses/>.
-"""
-"""
-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()
--- a/cubicweb/test/unittest_schema.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_schema.py Mon Mar 20 10:28:01 2017 +0100
@@ -17,7 +17,7 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""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()
--- a/cubicweb/test/unittest_utils.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/test/unittest_utils.py Mon Mar 20 10:28:01 2017 +0100
@@ -33,7 +33,7 @@
from cubicweb import Binary, Unauthorized
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.utils import (make_uid, UStringIO, RepeatList, HTMLHead,
- QueryCache, parse_repo_uri)
+ QueryCache)
from cubicweb.entity import Entity
try:
@@ -59,18 +59,6 @@
d.add(uid)
-class TestParseRepoUri(TestCase):
-
- def test_parse_repo_uri(self):
- self.assertEqual(('inmemory', None, 'myapp'),
- parse_repo_uri('myapp'))
- self.assertEqual(('inmemory', None, 'myapp'),
- parse_repo_uri('inmemory://myapp'))
- with self.assertRaises(NotImplementedError):
- parse_repo_uri('foo://bar')
-
-
-
class TestQueryCache(TestCase):
def test_querycache(self):
c = QueryCache(ceiling=20)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/unittest_wfutils.py Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,115 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import copy
+
+from cubicweb.devtools import testlib
+from cubicweb.wfutils import setup_workflow
+
+
+class TestWFUtils(testlib.CubicWebTC):
+
+ defs = {
+ 'group': {
+ 'etypes': 'CWGroup',
+ 'default': True,
+ 'initial_state': u'draft',
+ 'states': [u'draft', u'published'],
+ 'transitions': {
+ u'publish': {
+ 'fromstates': u'draft',
+ 'tostate': u'published',
+ 'requiredgroups': u'managers'
+ }
+ }
+ }
+ }
+
+ def test_create_workflow(self):
+ with self.admin_access.cnx() as cnx:
+ wf = setup_workflow(cnx, 'group', self.defs['group'])
+ self.assertEqual(wf.name, 'group')
+ self.assertEqual(wf.initial.name, u'draft')
+
+ draft = wf.state_by_name(u'draft')
+ self.assertIsNotNone(draft)
+
+ published = wf.state_by_name(u'published')
+ self.assertIsNotNone(published)
+
+ publish = wf.transition_by_name(u'publish')
+ self.assertIsNotNone(publish)
+
+ self.assertEqual(publish.destination_state, (published, ))
+ self.assertEqual(draft.allowed_transition, (publish, ))
+
+ self.assertEqual(
+ {g.name for g in publish.require_group},
+ {'managers'})
+
+ def test_update(self):
+ with self.admin_access.cnx() as cnx:
+ wf = setup_workflow(cnx, 'group', self.defs['group'])
+ eid = wf.eid
+
+ with self.admin_access.cnx() as cnx:
+ wfdef = copy.deepcopy(self.defs['group'])
+ wfdef['states'].append('new')
+ wfdef['initial_state'] = 'new'
+ wfdef['transitions'][u'publish']['fromstates'] = ('draft', 'new')
+ wfdef['transitions'][u'publish']['requiredgroups'] = (
+ u'managers', u'users')
+ wfdef['transitions'][u'todraft'] = {
+ 'fromstates': ('new', 'published'),
+ 'tostate': 'draft',
+ }
+
+ wf = setup_workflow(cnx, 'group', wfdef)
+ self.assertEqual(wf.eid, eid)
+ self.assertEqual(wf.name, 'group')
+ self.assertEqual(wf.initial.name, u'new')
+
+ new = wf.state_by_name(u'new')
+ self.assertIsNotNone(new)
+
+ draft = wf.state_by_name(u'draft')
+ self.assertIsNotNone(draft)
+
+ published = wf.state_by_name(u'published')
+ self.assertIsNotNone(published)
+
+ publish = wf.transition_by_name(u'publish')
+ self.assertIsNotNone(publish)
+
+ todraft = wf.transition_by_name(u'todraft')
+ self.assertIsNotNone(todraft)
+
+ self.assertEqual(
+ {g.name for g in publish.require_group},
+ {'managers', 'users'})
+
+ self.assertEqual(publish.destination_state, (published, ))
+ self.assertEqual(draft.allowed_transition, (publish, ))
+ self.assertEqual(todraft.destination_state, (draft, ))
+ self.assertEqual(published.allowed_transition, (todraft, ))
+ self.assertCountEqual(new.allowed_transition, (publish, todraft))
+
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main()
--- a/cubicweb/utils.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/utils.py Mon Mar 20 10:28:01 2017 +0100
@@ -19,8 +19,6 @@
from __future__ import division
-
-
import base64
import decimal
import datetime
@@ -37,7 +35,6 @@
from logging import getLogger
from six import text_type
-from six.moves.urllib.parse import urlparse
from logilab.mtconverter import xml_escape
from logilab.common.deprecation import deprecated
@@ -51,18 +48,19 @@
# initialize random seed from current time
random.seed()
+
def admincnx(appid):
+ from cubicweb import repoapi
from cubicweb.cwconfig import CubicWebConfiguration
from cubicweb.server.repository import Repository
- from cubicweb.server.utils import TasksManager
config = CubicWebConfiguration.config_for(appid)
login = config.default_admin_config['login']
password = config.default_admin_config['password']
- repo = Repository(config, TasksManager())
- session = repo.new_session(login, password=password)
- return session.new_cnx()
+ repo = Repository(config)
+ repo.bootstrap()
+ return repoapi.connect(repo, login, password=password)
def make_uid(key=None):
@@ -585,21 +583,6 @@
return 'javascript: ' + PERCENT_IN_URLQUOTE_RE.sub(r'%25', javascript_code)
-def parse_repo_uri(uri):
- """ transform a command line uri into a (protocol, hostport, appid), e.g:
- <myapp> -> 'inmemory', None, '<myapp>'
- inmemory://<myapp> -> 'inmemory', None, '<myapp>'
- """
- parseduri = urlparse(uri)
- scheme = parseduri.scheme
- if scheme == '':
- return ('inmemory', None, parseduri.path)
- if scheme == 'inmemory':
- return (scheme, None, parseduri.netloc)
- raise NotImplementedError('URI protocol not implemented for `%s`' % uri)
-
-
-
logger = getLogger('cubicweb.utils')
class QueryCache(object):
--- a/cubicweb/web/application.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/application.py Mon Mar 20 10:28:01 2017 +0100
@@ -18,7 +18,6 @@
"""CubicWeb web client application object"""
-
import contextlib
from functools import wraps
import json
@@ -77,10 +76,14 @@
@contextmanager
def anonymized_request(req):
+ from cubicweb.web.views.authentication import Session
+
orig_cnx = req.cnx
anon_cnx = anonymous_cnx(orig_cnx.session.repo)
try:
with anon_cnx:
+ # web request expect a session attribute on cnx referencing the web session
+ anon_cnx.session = Session(orig_cnx.session.repo, anon_cnx.user)
req.set_cnx(anon_cnx)
yield req
finally:
@@ -125,8 +128,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 +159,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 +253,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 +335,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
--- a/cubicweb/web/data/cubicweb.edition.js Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/data/cubicweb.edition.js Mon Mar 20 10:28:01 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');
}
--- a/cubicweb/web/formfields.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/formfields.py Mon Mar 20 10:28:01 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)
--- a/cubicweb/web/formwidgets.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/formwidgets.py Mon Mar 20 10:28:01 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
"""
@@ -803,17 +795,17 @@
req = form._cw
datestr = req.form.get(field.input_name(form, 'date'))
if datestr:
- datestr = datestr.strip() or None
- timestr = req.form.get(field.input_name(form, 'time'))
- if timestr:
- timestr = timestr.strip() or None
- if datestr is None:
+ datestr = datestr.strip()
+ if not datestr:
return None
try:
date = todatetime(req.parse_datetime(datestr, 'Date'))
except ValueError as exc:
raise ProcessFormError(text_type(exc))
- if timestr is None:
+ timestr = req.form.get(field.input_name(form, 'time'))
+ if timestr:
+ timestr = timestr.strip()
+ if not timestr:
return date
try:
time = req.parse_datetime(timestr, 'Time')
@@ -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'<table><tr><th>',
req._('i18n_bookmark_url_path'),
@@ -1012,8 +1002,6 @@
u'</th><td>']
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:
--- a/cubicweb/web/propertysheet.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/propertysheet.py Mon Mar 20 10:28:01 2017 +0100
@@ -19,6 +19,7 @@
+import errno
import re
import os
import os.path as osp
@@ -109,7 +110,9 @@
stream.write(content)
try:
os.rename(tmpfile, cachefile)
- except IOError:
+ except OSError as err:
+ if err.errno != errno.EEXIST:
+ raise
# Under windows, os.rename won't overwrite an existing file
os.unlink(cachefile)
os.rename(tmpfile, cachefile)
--- a/cubicweb/web/request.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/request.py Mon Mar 20 10:28:01 2017 +0100
@@ -105,28 +105,21 @@
"""
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
+ # some config (i.e. "pyramid") do not have "uiprops" nor "datadir_url"
+ # attributes)
+ self.uiprops = getattr(vreg.config, 'uiprops', None)
+ self.datadir_url = getattr(vreg.config, 'datadir_url', None)
#: enable UStringIO's write tracing
self.tracehtml = False
if vreg.config.debugmode:
@@ -179,22 +172,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 +192,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 +929,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 +937,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 +985,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
--- a/cubicweb/web/test/unittest_application.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_application.py Mon Mar 20 10:28:01 2017 +0100
@@ -24,7 +24,7 @@
from six.moves.http_cookies import SimpleCookie
from logilab.common.testlib import TestCase, unittest_main
-from logilab.common.decorators import clear_cache, classproperty
+from logilab.common.decorators import clear_cache
from cubicweb import view
from cubicweb.devtools.testlib import CubicWebTC, real_error_handling
@@ -32,7 +32,6 @@
from cubicweb.web import LogOut, Redirect, INTERNAL_FIELD_VALUE
from cubicweb.web.views.basecontrollers import ViewController
from cubicweb.web.application import anonymized_request
-from cubicweb import repoapi
class FakeMapping:
@@ -219,7 +218,7 @@
def test_handle_request_with_lang_fromurl(self):
"""No language negociation, get language from URL."""
self.config.global_set_option('language-mode', 'url-prefix')
- req, origsession = self.init_authentication('http')
+ req = self.init_authentication('http')
self.assertEqual(req.url(), 'http://testing.fr/cubicweb/login')
self.assertEqual(req.lang, 'en')
self.app.handle_request(req)
@@ -503,7 +502,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 +541,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
@@ -574,13 +573,13 @@
perm_eid = text_type(perm.eid)
req.form = {
'eid': [dir_eid, perm_eid],
- '__maineid' : dir_eid,
+ '__maineid': dir_eid,
'__type:%s' % dir_eid: 'Directory',
'__type:%s' % perm_eid: 'DirectoryPermission',
'_cw_entity_fields:%s' % dir_eid: '',
'_cw_entity_fields:%s' % perm_eid: 'has_permission-object',
'has_permission-object:%s' % perm_eid: '',
- }
+ }
path, _params = self.expect_redirect_handle_request(req, 'edit')
self.assertTrue(req.find('Directory', eid=mydir.eid))
self.assertFalse(req.find('DirectoryPermission', eid=perm.eid))
@@ -638,20 +637,20 @@
# authentication tests ####################################################
def test_http_auth_no_anon(self):
- req, origsession = self.init_authentication('http')
+ req = self.init_authentication('http')
self.assertAuthFailure(req)
self.app.handle_request(req)
self.assertEqual(401, req.status_out)
clear_cache(req, 'get_authorization')
authstr = base64.encodestring(('%s:%s' % (self.admlogin, self.admpassword)).encode('ascii'))
req.set_request_header('Authorization', 'basic %s' % authstr.decode('ascii'))
- self.assertAuthSuccess(req, origsession)
+ self.assertAuthSuccess(req)
req._url = 'logout'
self.assertRaises(LogOut, self.app_handle_request, req)
self.assertEqual(len(self.open_sessions), 0)
def test_cookie_auth_no_anon(self):
- req, origsession = self.init_authentication('cookie')
+ req = self.init_authentication('cookie')
self.assertAuthFailure(req)
try:
form = self.app.handle_request(req)
@@ -663,7 +662,7 @@
self.assertFalse(req.cnx) # Mock cnx are False
req.form['__login'] = self.admlogin
req.form['__password'] = self.admpassword
- self.assertAuthSuccess(req, origsession)
+ self.assertAuthSuccess(req)
req._url = 'logout'
self.assertRaises(LogOut, self.app_handle_request, req)
self.assertEqual(len(self.open_sessions), 0)
@@ -675,11 +674,11 @@
cnx.execute('INSERT EmailAddress X: X address %(address)s, U primary_email X '
'WHERE U login %(login)s', {'address': address, 'login': login})
cnx.commit()
- req, origsession = self.init_authentication('cookie')
+ req = self.init_authentication('cookie')
self.set_option('allow-email-login', True)
req.form['__login'] = address
req.form['__password'] = self.admpassword
- self.assertAuthSuccess(req, origsession)
+ self.assertAuthSuccess(req)
req._url = 'logout'
self.assertRaises(LogOut, self.app_handle_request, req)
self.assertEqual(len(self.open_sessions), 0)
@@ -701,7 +700,7 @@
with cnx:
req.set_cnx(cnx)
self.assertEqual(len(self.open_sessions), 1)
- self.assertEqual(asession.login, 'anon')
+ self.assertEqual(asession.user.login, 'anon')
self.assertTrue(asession.anonymous_session)
self._reset_cookie(req)
@@ -718,27 +717,27 @@
self._reset_cookie(req)
def test_http_auth_anon_allowed(self):
- req, origsession = self.init_authentication('http', 'anon')
+ req = self.init_authentication('http', 'anon')
self._test_auth_anon(req)
authstr = base64.encodestring(b'toto:pouet')
req.set_request_header('Authorization', 'basic %s' % authstr.decode('ascii'))
self._test_anon_auth_fail(req)
authstr = base64.encodestring(('%s:%s' % (self.admlogin, self.admpassword)).encode('ascii'))
req.set_request_header('Authorization', 'basic %s' % authstr.decode('ascii'))
- self.assertAuthSuccess(req, origsession)
+ self.assertAuthSuccess(req)
req._url = 'logout'
self.assertRaises(LogOut, self.app_handle_request, req)
self.assertEqual(len(self.open_sessions), 0)
def test_cookie_auth_anon_allowed(self):
- req, origsession = self.init_authentication('cookie', 'anon')
+ req = self.init_authentication('cookie', 'anon')
self._test_auth_anon(req)
req.form['__login'] = 'toto'
req.form['__password'] = 'pouet'
self._test_anon_auth_fail(req)
req.form['__login'] = self.admlogin
req.form['__password'] = self.admpassword
- self.assertAuthSuccess(req, origsession)
+ self.assertAuthSuccess(req)
req._url = 'logout'
self.assertRaises(LogOut, self.app_handle_request, req)
self.assertEqual(0, len(self.open_sessions))
@@ -749,10 +748,10 @@
# admin should see anon + admin
self.assertEqual(2, len(list(req.find('CWUser'))))
with anonymized_request(req):
- self.assertEqual('anon', req.session.login, 'anon')
+ self.assertEqual('anon', req.session.user.login)
# anon should only see anon user
self.assertEqual(1, len(list(req.find('CWUser'))))
- self.assertEqual(self.admlogin, req.session.login)
+ self.assertEqual(self.admlogin, req.session.user.login)
self.assertEqual(2, len(list(req.find('CWUser'))))
def test_non_regr_optional_first_var(self):
--- a/cubicweb/web/test/unittest_form.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_form.py Mon Mar 20 10:28:01 2017 +0100
@@ -18,14 +18,12 @@
import time
from datetime import datetime
-import pytz
-
-from xml.etree.ElementTree import fromstring
-from lxml import html
from six import text_type
-from logilab.common.testlib import unittest_main
+import pytz
+
+from lxml import html
from cubicweb import Binary, ValidationError
from cubicweb.mttransforms import HAS_TAL
@@ -34,7 +32,7 @@
PasswordField, DateTimeField,
FileField, EditableFileField,
TZDatetimeField)
-from cubicweb.web.formwidgets import PasswordInput, Input, DateTimePicker
+from cubicweb.web.formwidgets import DateTimePicker, JQueryDateTimePicker
from cubicweb.web.views.forms import EntityFieldsForm, FieldsForm
from cubicweb.web.views.workflow import ChangeStateForm
from cubicweb.web.views.formrenderers import FormRenderer
@@ -64,6 +62,15 @@
form = AForm(req)
self.assertRaises(ValidationError, form.process_posted)
+ def test_jqdt_process_data(self):
+ widget = JQueryDateTimePicker()
+ field = DateTimeField('jqdt')
+ with self.admin_access.web_request(**{'jqdt-date': '', 'jqdt-time': '00:00'}) as req:
+ self._cw = req
+ self.formvalues = {}
+ date = widget.process_field_data(self, field)
+ self.assertIsNone(date)
+
class EntityFieldsFormTC(CubicWebTC):
@@ -200,20 +207,20 @@
def test_richtextfield_1(self):
with self.admin_access.web_request() as req:
req.use_fckeditor = lambda: False
- self._test_richtextfield(req, '''<select id="description_format-subject:%(eid)s" name="description_format-subject:%(eid)s" size="1" style="display: block" tabindex="1">
+ self._test_richtextfield(req, '''<select id="description_format-subject:%(eid)s" name="description_format-subject:%(eid)s" size="1" style="display: block">
''' + ('<option value="text/cubicweb-page-template">text/cubicweb-page-template</option>\n'
if HAS_TAL else '') +
'''<option selected="selected" value="text/html">text/html</option>
<option value="text/markdown">text/markdown</option>
<option value="text/plain">text/plain</option>
<option value="text/rest">text/rest</option>
-</select><textarea cols="80" id="description-subject:%(eid)s" name="description-subject:%(eid)s" onkeyup="autogrow(this)" rows="2" tabindex="2"></textarea>''')
+</select><textarea cols="80" id="description-subject:%(eid)s" name="description-subject:%(eid)s" onkeyup="autogrow(this)" rows="2"></textarea>''')
def test_richtextfield_2(self):
with self.admin_access.web_request() as req:
req.use_fckeditor = lambda: True
- self._test_richtextfield(req, '<input name="description_format-subject:%(eid)s" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:%(eid)s" name="description-subject:%(eid)s" onkeyup="autogrow(this)" rows="2" tabindex="1"></textarea>')
+ self._test_richtextfield(req, '<input name="description_format-subject:%(eid)s" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:%(eid)s" name="description-subject:%(eid)s" onkeyup="autogrow(this)" rows="2"></textarea>')
def test_filefield(self):
@@ -229,11 +236,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),
- '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" tabindex="1" type="file" value="" />
+ '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" type="file" value="" />
<a href="javascript: toggleVisibility('data-subject:%(eid)s-advanced')" title="show advanced fields"><img src="http://testing.fr/cubicweb/data/puce_down.png" alt="show advanced fields"/></a>
<div id="data-subject:%(eid)s-advanced" class="hidden">
-<label for="data_format-subject:%(eid)s">data_format</label><input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" tabindex="2" type="text" value="text/plain" /><br/>
-<label for="data_encoding-subject:%(eid)s">data_encoding</label><input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" tabindex="3" type="text" value="UTF-8" /><br/>
+<label for="data_format-subject:%(eid)s">data_format</label><input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" type="text" value="text/plain" /><br/>
+<label for="data_encoding-subject:%(eid)s">data_encoding</label><input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" type="text" value="UTF-8" /><br/>
</div>
<br/>
<input name="data-subject__detach:%(eid)s" type="checkbox" />
@@ -253,17 +260,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),
- '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" tabindex="1" type="file" value="" />
+ '''<input id="data-subject:%(eid)s" name="data-subject:%(eid)s" type="file" value="" />
<a href="javascript: toggleVisibility('data-subject:%(eid)s-advanced')" title="show advanced fields"><img src="http://testing.fr/cubicweb/data/puce_down.png" alt="show advanced fields"/></a>
<div id="data-subject:%(eid)s-advanced" class="hidden">
-<label for="data_format-subject:%(eid)s">data_format</label><input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" tabindex="2" type="text" value="text/plain" /><br/>
-<label for="data_encoding-subject:%(eid)s">data_encoding</label><input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" tabindex="3" type="text" value="UTF-8" /><br/>
+<label for="data_format-subject:%(eid)s">data_format</label><input id="data_format-subject:%(eid)s" maxlength="50" name="data_format-subject:%(eid)s" size="45" type="text" value="text/plain" /><br/>
+<label for="data_encoding-subject:%(eid)s">data_encoding</label><input id="data_encoding-subject:%(eid)s" maxlength="20" name="data_encoding-subject:%(eid)s" size="20" type="text" value="UTF-8" /><br/>
</div>
<br/>
<input name="data-subject__detach:%(eid)s" type="checkbox" />
detach attached file
<p><b>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.</b></p>
-<textarea cols="80" name="data-subject:%(eid)s" onkeyup="autogrow(this)" rows="3" tabindex="4">new widgets system</textarea>''' % {'eid': file.eid})
+<textarea cols="80" name="data-subject:%(eid)s" onkeyup="autogrow(this)" rows="3">new widgets system</textarea>''' % {'eid': file.eid})
def _modified_tzdatenaiss(self, eid, date_and_time_str=None):
ctx = {}
@@ -303,9 +310,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),
- '''<input id="upassword-subject:%(eid)s" name="upassword-subject:%(eid)s" tabindex="1" type="password" value="" />
+ '''<input id="upassword-subject:%(eid)s" name="upassword-subject:%(eid)s" type="password" value="" />
<br/>
-<input name="upassword-subject-confirm:%(eid)s" tabindex="1" type="password" value="" />
+<input name="upassword-subject-confirm:%(eid)s" type="password" value="" />
 
<span class="emphasis">confirm password</span>''' % {'eid': req.user.eid})
@@ -319,4 +326,5 @@
# self.assertEqual(init, cur)
if __name__ == '__main__':
- unittest_main()
+ import unittest
+ unittest.main()
--- a/cubicweb/web/test/unittest_formwidgets.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_formwidgets.py Mon Mar 20 10:28:01 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),
- '<label><input id="bool" name="bool" tabindex="1" '
+ '<label><input id="bool" name="bool" '
'type="checkbox" value="1" /> '
'python >> others</label>')
--- a/cubicweb/web/test/unittest_http_headers.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_http_headers.py Mon Mar 20 10:28:01 2017 +0100
@@ -13,6 +13,7 @@
with self.assertRaises(ValueError):
http_headers.generateTrueFalse('any value')
+
if __name__ == '__main__':
from unittest import main
main()
--- a/cubicweb/web/test/unittest_reledit.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_reledit.py Mon Mar 20 10:28:01 2017 +0100
@@ -73,13 +73,13 @@
<table class="">
<tr class="title_subject_row">
<td>
-<input id="title-subject:%(eid)s" maxlength="32" name="title-subject:%(eid)s" size="32" tabindex="1" type="text" value="cubicweb-world-domination" />
+<input id="title-subject:%(eid)s" maxlength="32" name="title-subject:%(eid)s" size="32" type="text" value="cubicweb-world-domination" />
</td></tr>
</table></fieldset>
<table class="buttonbar">
<tr>
-<td><button class="validateButton" tabindex="2" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
-<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('title-subject-%(eid)s')" tabindex="3" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
+<td><button class="validateButton" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('title-subject-%(eid)s')" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
</tr></table>
</fieldset>
<iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
@@ -108,23 +108,23 @@
<tr class="title_subject_row">
<th class="labelCol"><label class="required" for="title-subject:A">title</label></th>
<td>
-<input id="title-subject:A" maxlength="50" name="title-subject:A" size="45" tabindex="4" type="text" value="" />
+<input id="title-subject:A" maxlength="50" name="title-subject:A" size="45" type="text" value="" />
</td></tr>
<tr class="description_subject_row">
<th class="labelCol"><label for="description-subject:A">description</label></th>
<td>
-<input name="description_format-subject:A" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:A" name="description-subject:A" onkeyup="autogrow(this)" rows="2" tabindex="5"></textarea>
+<input name="description_format-subject:A" type="hidden" value="text/html" /><textarea cols="80" cubicweb:type="wysiwyg" id="description-subject:A" name="description-subject:A" onkeyup="autogrow(this)" rows="2"></textarea>
</td></tr>
<tr class="rss_url_subject_row">
<th class="labelCol"><label for="rss_url-subject:A">rss_url</label></th>
<td>
-<input id="rss_url-subject:A" maxlength="128" name="rss_url-subject:A" size="45" tabindex="6" type="text" value="" />
+<input id="rss_url-subject:A" maxlength="128" name="rss_url-subject:A" size="45" type="text" value="" />
</td></tr>
</table></fieldset>
<table class="buttonbar">
<tr>
-<td><button class="validateButton" tabindex="7" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
-<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('long_desc-subject-%(eid)s')" tabindex="8" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
+<td><button class="validateButton" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('long_desc-subject-%(eid)s')" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
</tr></table>
</fieldset>
<iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
@@ -152,7 +152,7 @@
<table class="">
<tr class="manager_subject_row">
<td>
-<select id="manager-subject:%(eid)s" name="manager-subject:%(eid)s" size="1" tabindex="9">
+<select id="manager-subject:%(eid)s" name="manager-subject:%(eid)s" size="1">
<option value="__cubicweb_internal_field__"></option>
<option value="%(toto)s">Toto</option>
</select>
@@ -160,8 +160,8 @@
</table></fieldset>
<table class="buttonbar">
<tr>
-<td><button class="validateButton" tabindex="10" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
-<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('manager-subject-%(eid)s')" tabindex="11" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
+<td><button class="validateButton" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
+<td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel('manager-subject-%(eid)s')" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
</tr></table>
</fieldset>
<iframe width="0px" height="0px" src="javascript: void(0);" name="eformframe" id="eformframe"></iframe>
--- a/cubicweb/web/test/unittest_request.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_request.py Mon Mar 20 10:28:01 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'
--- a/cubicweb/web/test/unittest_uicfg.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_uicfg.py Mon Mar 20 10:28:01 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 <http://www.gnu.org/licenses/>.
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'])
--- a/cubicweb/web/test/unittest_views_basecontrollers.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_views_basecontrollers.py Mon Mar 20 10:28:01 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]
@@ -835,7 +884,8 @@
(self.schema['tags'].rdefs['Tag', 'CWUser'],
{'delete': (RRQLExpression('S owned_by U'), )}, )):
with self.admin_access.web_request(rql='CWUser P WHERE P login "John"',
- pageid='123', fname='view') as req:
+ pageid='123', fname='view',
+ session=req.session) as req:
ctrl = self.ctrl(req)
rset = self.john.as_rset()
rset.req = req
@@ -849,7 +899,8 @@
self.assertEqual(deletes, [])
inserts = get_pending_inserts(req)
self.assertEqual(inserts, ['12:tags:13'])
- with self.remote_calling('add_pending_inserts', [['12', 'tags', '14']]) as (_, req):
+ with self.remote_calling('add_pending_inserts', [['12', 'tags', '14']],
+ session=req.session) as (_, req):
deletes = get_pending_deletes(req)
self.assertEqual(deletes, [])
inserts = get_pending_inserts(req)
@@ -868,7 +919,8 @@
self.assertEqual(inserts, [])
deletes = get_pending_deletes(req)
self.assertEqual(deletes, ['12:tags:13'])
- with self.remote_calling('add_pending_delete', ['12', 'tags', '14']) as (_, req):
+ with self.remote_calling('add_pending_delete', ['12', 'tags', '14'],
+ session=req.session) as (_, req):
inserts = get_pending_inserts(req)
self.assertEqual(inserts, [])
deletes = get_pending_deletes(req)
@@ -882,9 +934,10 @@
req.remove_pending_operations()
def test_remove_pending_operations(self):
- with self.remote_calling('add_pending_delete', ['12', 'tags', '13']):
+ with self.remote_calling('add_pending_delete', ['12', 'tags', '13']) as (_, req):
pass
- with self.remote_calling('add_pending_inserts', [['12', 'tags', '14']]) as (_, req):
+ with self.remote_calling('add_pending_inserts', [['12', 'tags', '14']],
+ session=req.session) as (_, req):
inserts = get_pending_inserts(req)
self.assertEqual(inserts, ['12:tags:14'])
deletes = get_pending_deletes(req)
--- a/cubicweb/web/test/unittest_views_baseviews.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_views_baseviews.py Mon Mar 20 10:28:01 2017 +0100
@@ -156,5 +156,138 @@
b'<head>'],
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 "<script></script>"')
+ 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'<script></script>', upassword=u'toto')
+ result = entity.view('incontext')
+ expected = (u'<a href="http://testing.fr/cubicweb/%d" title="">'
+ u'<script></script></a>' % 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'<script></script>', upassword=u'toto')
+ result = entity.view('outofcontext')
+ expect = (u'<a href="http://testing.fr/cubicweb/%d" title="">'
+ u'<script></script></a>' % 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'<script></script>', upassword=u'toto')
+ result = entity.view('oneline')
+ expect = (u'<a href="http://testing.fr/cubicweb/%d" title="">'
+ u'<script></script></a>' % 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'<script></script>', upassword=u'toto')
+ result = entity.view('text')
+ self.assertEqual(result, u'<script></script>')
+
+ def test_textincontext(self):
+ with self.admin_access.web_request() as req:
+ entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+ result = entity.view('textincontext')
+ self.assertEqual(result, u'<script></script>')
+
+ def test_textoutofcontext(self):
+ with self.admin_access.web_request() as req:
+ entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+ result = entity.view('textoutofcontext')
+ self.assertEqual(result, u'<script></script>')
+
+ def test_list(self):
+ with self.admin_access.web_request() as req:
+ entity = req.create_entity('CWUser', login=u'<script></script>', upassword=u'toto')
+ rset = req.execute('Any X WHERE X is CWUser')
+ result = req.view('list', rset)
+ expected = u'''<ul class="section">
+<li><a href="http://testing.fr/cubicweb/%d" title=""><script></script></a></li>
+<li><a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></li>
+<li><a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></li>
+</ul>
+''' % 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'<script></script>', upassword=u'toto')
+ rset = req.execute('Any X WHERE X is CWUser')
+ result = req.view('simplelist', rset)
+ expected = (
+ u'<div class="section">'
+ u'<a href="http://testing.fr/cubicweb/%d" title="">'
+ u'<script></script></a></div>'
+ u'<div class="section">'
+ u'<a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div>'
+ u'<div class="section">'
+ u'<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div>'
+ % 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'<script></script>', upassword=u'toto')
+ rset = req.execute('Any X WHERE X is CWUser')
+ result = req.view('sameetypelist', rset)
+ expected = (
+ u'<h1>CWUser_plural</h1>'
+ u'<div class="section">'
+ u'<a href="http://testing.fr/cubicweb/%d" title="">'
+ u'<script></script></a></div>'
+ u'<div class="section">'
+ u'<a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div>'
+ u'<div class="section">'
+ u'<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div>'
+ % 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'<script></script>', upassword=u'toto')
+ rset = req.execute('Any X WHERE X is CWUser')
+ result = req.view('csv', rset)
+ expected = (
+ u'<a href="http://testing.fr/cubicweb/%d" title=""><script></script></a>, '
+ u'<a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a>, '
+ u'<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a>'
+ % 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'<script></script>', 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'<div>CWUser #%d - <span>latest update on</span>'
+ u' <span class="value">2015/01/01</span>,'
+ u' <span>created on</span>'
+ u' <span class="value">2000/01/01</span></div>'
+ % entity.eid
+ )
+ self.assertEqual(result, expected)
+
+
if __name__ == '__main__':
unittest_main()
--- a/cubicweb/web/test/unittest_views_editforms.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_views_editforms.py Mon Mar 20 10:28:01 2017 +0100
@@ -20,7 +20,9 @@
from logilab.common import tempattr
from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.predicates import is_instance
from cubicweb.web.views import uicfg
+from cubicweb.web.views.editforms import DeleteConfFormView
from cubicweb.web.formwidgets import AutoCompletionWidget
from cubicweb.schema import RRQLExpression
@@ -239,6 +241,59 @@
rset = req.execute('CWGroup X')
self.view('deleteconf', rset, template=None, req=req).source
+ def test_delete_conf_formview_composite(self):
+ with self.admin_access.cnx() as cnx:
+ d1 = cnx.create_entity('Directory', name=u'dtest1')
+ d2 = cnx.create_entity('Directory', name=u'dtest2', parent=d1)
+ d3 = cnx.create_entity('Directory', name=u'dtest3', parent=d2)
+ d4 = cnx.create_entity('Directory', name=u'dtest4', parent=d1)
+ for i in range(3):
+ cnx.create_entity('Directory', name=u'child%s' % (i,),
+ parent=d3)
+ cnx.commit()
+
+ class DirectoryDeleteView(DeleteConfFormView):
+ __select__ = (DeleteConfFormView.__select__ &
+ is_instance('Directory'))
+ show_composite = True
+
+ self.vreg['propertyvalues']['navigation.page-size'] = 3
+ with self.admin_access.web_request() as req, \
+ self.temporary_appobjects(DirectoryDeleteView):
+ rset = req.execute('Directory X WHERE X name "dtest1"')
+ source = self.view('deleteconf', rset,
+ template=None, req=req).source.decode('utf-8')
+ # Show composite object at depth 1
+ # Don't display "And more composite entities" since their are equal
+ # to page size
+ expected = (
+ '<li>'
+ '<a href="http://testing.fr/cubicweb/directory/%s">dtest1</a>'
+ '<ul class="treeview"><li>'
+ '<a href="http://testing.fr/cubicweb/directory/%s">dtest4</a>'
+ '</li><li class="last">'
+ '<a href="http://testing.fr/cubicweb/directory/%s">dtest2</a>'
+ '</li></ul></li>') % (d1.eid, d4.eid, d2.eid)
+ self.assertIn(expected, source)
+
+ # Page size is reached, show "And more composite entities"
+ rset = req.execute('Directory X WHERE X name "dtest3"')
+ source = self.view('deleteconf', rset,
+ template=None, req=req).source.decode('utf-8')
+ expected = (
+ '<li>'
+ '<a href="http://testing.fr/cubicweb/directory/%s">dtest3</a>'
+ '<ul class="treeview"><li>'
+ '<a href="http://testing.fr/cubicweb/directory/%s">child2</a>'
+ '</li><li>'
+ '<a href="http://testing.fr/cubicweb/directory/%s">child1</a>'
+ '</li><li class="last">And more composite entities</li>'
+ '</ul></li>') % (
+ d3.eid,
+ req.find('Directory', name='child2').one().eid,
+ req.find('Directory', name='child1').one().eid)
+ self.assertIn(expected, source)
+
def test_automatic_edition_formview(self):
with self.admin_access.web_request() as req:
rset = req.execute('CWUser X')
--- a/cubicweb/web/test/unittest_views_forms.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_views_forms.py Mon Mar 20 10:28:01 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()
--- a/cubicweb/web/test/unittest_views_searchrestriction.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/test/unittest_views_searchrestriction.py Mon Mar 20 10:28:01 2017 +0100
@@ -23,8 +23,9 @@
class InsertAttrRelationTC(CubicWebTC):
def parse(self, query):
- rqlst = self.vreg.parse(self.session, query)
- select = rqlst.children[0]
+ with self.admin_access.cnx() as cnx:
+ rqlst = self.vreg.parse(cnx, query)
+ rqlst.children[0]
return rqlst
def _generate(self, rqlst, rel, role, attr):
--- a/cubicweb/web/views/actions.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/actions.py Mon Mar 20 10:28:01 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
--- a/cubicweb/web/views/authentication.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/authentication.py Mon Mar 20 10:28:01 2017 +0100
@@ -17,16 +17,18 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""user authentication component"""
-
-
from logilab.common.deprecation import class_renamed
+from logilab.common.textutils import unormalize
from cubicweb import AuthenticationError
+from cubicweb.utils import make_uid
from cubicweb.view import Component
from cubicweb.web import InvalidSession
+from cubicweb.server.session import Connection
-class NoAuthInfo(Exception): pass
+class NoAuthInfo(Exception):
+ pass
class WebAuthInfoRetriever(Component):
@@ -67,6 +69,7 @@
"""
pass
+
WebAuthInfoRetreiver = class_renamed(
'WebAuthInfoRetreiver', WebAuthInfoRetriever,
'[3.17] WebAuthInfoRetreiver had been renamed into WebAuthInfoRetriever '
@@ -92,12 +95,45 @@
def revalidate_login(self, req):
return req.get_authorization()[0]
+
LoginPasswordRetreiver = class_renamed(
'LoginPasswordRetreiver', LoginPasswordRetriever,
'[3.17] LoginPasswordRetreiver had been renamed into LoginPasswordRetriever '
'("ie" instead of "ei")')
+class Session(object):
+ """In-memory user session
+ """
+
+ def __init__(self, repo, user):
+ self.user = user # XXX deprecate and store only a login.
+ self.repo = repo
+ self.sessionid = make_uid(unormalize(user.login))
+ self.data = {}
+
+ def __unicode__(self):
+ return '<session %s (0x%x)>' % (unicode(self.user.login), id(self))
+
+ @property
+ def anonymous_session(self):
+ # XXX for now, anonymous_user only exists in webconfig (and testconfig).
+ # It will only be present inside all-in-one instance.
+ # there is plan to move it down to global config.
+ if not hasattr(self.repo.config, 'anonymous_user'):
+ # not a web or test config, no anonymous user
+ return False
+ return self.user.login == self.repo.config.anonymous_user()[0]
+
+ def new_cnx(self):
+ """Return a new Connection object linked to the session
+
+ The returned Connection will *not* be managed by the Session.
+ """
+ cnx = Connection(self.repo, self.user)
+ cnx.session = self
+ return cnx
+
class RepositoryAuthenticationManager(object):
"""authenticate user associated to a request and check session validity"""
@@ -133,7 +169,7 @@
# check session.login and not user.login, since in case of login by
# email, login and cnx.login are the email while user.login is the
# actual user login
- if login and session.login != login:
+ if login and session.user.login != login:
raise InvalidSession('login mismatch')
def authenticate(self, req):
@@ -155,7 +191,7 @@
session = self._authenticate(login, authinfo)
except AuthenticationError:
retriever.cleanup_authentication_information(req)
- continue # the next one may succeed
+ continue # the next one may succeed
for retriever_ in self.authinforetrievers:
retriever_.authenticated(retriever, req, session, login, authinfo)
return session, login
@@ -170,4 +206,6 @@
raise AuthenticationError()
def _authenticate(self, login, authinfo):
- return self.repo.new_session(login, **authinfo)
+ with self.repo.internal_cnx() as cnx:
+ user = self.repo.authenticate_user(cnx, login, **authinfo)
+ return Session(self.repo, user)
--- a/cubicweb/web/views/autoform.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/autoform.py Mon Mar 20 10:28:01 2017 +0100
@@ -565,9 +565,9 @@
w(u'</tr>')
w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid)
w(u'<th class="labelCol">')
- w(u'<select id="relationSelector_%s" tabindex="%s" '
+ w(u'<select id="relationSelector_%s" '
'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
- % (eid, req.next_tabindex(), xml_escape(json_dumps(eid))))
+ % (eid, xml_escape(json_dumps(eid))))
w(u'<option value="">%s</option>' % _('select a relation'))
for i18nrtype, rschema, role in field.relations:
# more entities to link to
@@ -861,24 +861,25 @@
"""return a list of (relation schema, role) to edit for the entity"""
if self.display_fields is not None:
schema = self._cw.vreg.schema
- return [(schema[rtype], role) for rtype, role in self.display_fields]
+ for rtype, role in self.display_fields:
+ yield (schema[rtype], role)
if self.edited_entity.has_eid() and not self.edited_entity.cw_has_perm('update'):
- return []
+ return
action = 'update' if self.edited_entity.has_eid() else 'add'
- return [(rtype, role) for rtype, _, role in self._relations_by_section(
- 'attributes', action, strict)]
+ for rtype, _, role in self._relations_by_section('attributes', action, strict):
+ yield (rtype, role)
def editable_relations(self):
"""return a sorted list of (relation's label, relation'schema, role) for
relations in the 'relations' section
"""
- result = []
- for rschema, _, role in self._relations_by_section('relations',
- strict=True):
- result.append( (rschema.display_name(self.edited_entity._cw, role,
- self.edited_entity.cw_etype),
- rschema, role) )
- return sorted(result)
+ return sorted(self.iter_editable_relations())
+
+ def iter_editable_relations(self):
+ for rschema, _, role in self._relations_by_section('relations', strict=True):
+ yield (rschema.display_name(self.edited_entity._cw, role,
+ self.edited_entity.cw_etype),
+ rschema, role)
def inlined_relations(self):
"""return a list of (relation schema, target schemas, role) matching
@@ -889,10 +890,8 @@
# inlined forms control ####################################################
def inlined_form_views(self):
- """compute and return list of inlined form views (hosting the inlined
- form object)
+ """Yield inlined form views (hosting the inlined form object)
"""
- allformviews = []
entity = self.edited_entity
for rschema, ttypes, role in self.inlined_relations():
# show inline forms only if there's one possible target type
@@ -904,11 +903,15 @@
continue
tschema = ttypes[0]
ttype = tschema.type
- formviews = list(self.inline_edition_form_view(rschema, ttype, role))
+ existing = bool(entity.related(rschema, role)) if entity.has_eid() else False
+ for formview in self.inline_edition_form_view(rschema, ttype, role):
+ yield formview
+ existing = True
card = rschema.role_rdef(entity.e_schema, ttype, role).role_cardinality(role)
- existing = entity.related(rschema, role) if entity.has_eid() else formviews
if self.should_display_inline_creation_form(rschema, existing, card):
- formviews += self.inline_creation_form_view(rschema, ttype, role)
+ for formview in self.inline_creation_form_view(rschema, ttype, role):
+ yield formview
+ existing = True
# we can create more than one related entity, we thus display a link
# to add new related entities
if self.must_display_add_new_relation_link(rschema, role, tschema,
@@ -918,9 +921,7 @@
etype=ttype, rtype=rschema, role=role, card=card,
peid=self.edited_entity.eid,
petype=self.edited_entity.e_schema, pform=self)
- formviews.append(addnewlink)
- allformviews += formviews
- return allformviews
+ yield addnewlink
def should_display_inline_creation_form(self, rschema, existing, card):
"""return true if a creation form should be inlined
--- a/cubicweb/web/views/basecomponents.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/basecomponents.py Mon Mar 20 10:28:01 2017 +0100
@@ -66,9 +66,9 @@
self._cw.add_onload('$("#rql").autocomplete({source: "%s"});'
% (req.build_url('json', fname='rql_suggest')))
self.w(u'''<div id="rqlinput" class="%s"><form action="%s"><fieldset>
-<input type="text" id="rql" name="rql" value="%s" title="%s" tabindex="%s" accesskey="q" class="searchField" />
+<input type="text" id="rql" name="rql" value="%s" title="%s" accesskey="q" class="searchField" />
''' % (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'<input type="hidden" name="__mode" value="%s"/>'
% ':'.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)
--- a/cubicweb/web/views/basetemplates.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/basetemplates.py Mon Mar 20 10:28:01 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'</td>\n')
self.nav_column(view, 'right')
self.w(u'</tr></table></div>\n')
- self.wview('footer', rset=self.cw_rset)
+ self.wview('footer', rset=self.cw_rset, view=view)
self.w(u'</body>')
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'<td id="navColumn%s"><div class="navboxes">\n' % context.capitalize())
for box in boxes:
box.render(w=self.w, view=view)
@@ -248,7 +247,6 @@
def template_header(self, content_type, view=None, page_title='', additional_headers=()):
w = self.whead
- lang = self._cw.lang
self.write_doctype()
w(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n'
% (content_type, self._cw.encoding))
@@ -269,7 +267,6 @@
page_title = page_title or view.page_title()
additional_headers = additional_headers or view.html_headers()
whead = self.whead
- lang = self._cw.lang
self.write_doctype()
whead(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n'
% (content_type, self._cw.encoding))
@@ -337,10 +334,10 @@
def alternates(self):
urlgetter = self._cw.vreg['components'].select_or_none('rss_feed_url',
- self._cw, rset=self.cw_rset)
+ self._cw, rset=self.cw_rset)
if urlgetter is not None:
self.whead(u'<link rel="alternate" type="application/rss+xml" title="RSS feed" href="%s"/>\n'
- % xml_escape(urlgetter.feed_url()))
+ % xml_escape(urlgetter.feed_url()))
class HTMLPageHeader(View):
@@ -398,7 +395,8 @@
def footer_content(self):
actions = self._cw.vreg['actions'].possible_actions(self._cw,
- rset=self.cw_rset)
+ rset=self.cw_rset,
+ view=self.cw_extra_kwargs['view'])
footeractions = actions.get('footer', ())
for i, action in enumerate(footeractions):
self.w(u'<a href="%s">%s</a>' % (action.url(),
@@ -406,10 +404,9 @@
if i < (len(footeractions) - 1):
self.w(u' | ')
+
class HTMLContentHeader(View):
- """default html page content header:
- * include message component if selectable for this request
- * include selectable content navigation components
+ """default html page content header: include selectable content navigation components
"""
__regid__ = 'contentheader'
@@ -439,6 +436,7 @@
comp.render(w=self.w, view=view)
self.w(u'</div>')
+
class BaseLogForm(forms.FieldsForm):
"""Abstract Base login form to be used by any login form
"""
@@ -461,7 +459,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 +472,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 +487,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 +530,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)
--- a/cubicweb/web/views/boxes.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/boxes.py Mon Mar 20 10:28:01 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"""<form action="%(action)s">
<table id="%(id)s"><tr><td>
-<input class="norql" type="text" accesskey="q" tabindex="%(tabindex1)s" title="search text" value="%(value)s" name="rql" />
+<input class="norql" type="text" accesskey="q" title="search text" value="%(value)s" name="rql" />
<input type="hidden" name="__fromsearchbox" value="1" />
<input type="hidden" name="subvid" value="tsearch" />
</td><td>
-<input tabindex="%(tabindex2)s" type="submit" class="rqlsubmit" value="" />
+<input type="submit" class="rqlsubmit" value="" />
</td></tr></table>
</form>"""
@@ -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 ###################################################
--- a/cubicweb/web/views/calendar.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/calendar.py Mon Mar 20 10:28:01 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):
--- a/cubicweb/web/views/cwproperties.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/cwproperties.py Mon Mar 20 10:28:01 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
--- a/cubicweb/web/views/debug.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/debug.py Mon Mar 20 10:28:01 2017 +0100
@@ -91,7 +91,6 @@
w(u'<h2>%s</h2>' % _('Repository'))
w(u'<h3>%s</h3>' % _('resources usage'))
stats = self._cw.call_service('repo_stats')
- stats['looping_tasks'] = ', '.join('%s (%s seconds)' % (n, i) for n, i in stats['looping_tasks'])
stats['threads'] = ', '.join(sorted(stats['threads']))
for k in stats:
if k == 'type_cache_size':
@@ -104,22 +103,6 @@
pyvalue = [(sname, format_stat(sname, sval))
for sname, sval in sorted(stats.items())]
self.wview('pyvaltable', pyvalue=pyvalue, header_column_idx=0)
- # open repo sessions
- if req.user.is_in_group('managers'):
- w(u'<h3>%s</h3>' % _('opened sessions'))
- sessions = repo._sessions.values()
- if sessions:
- w(u'<ul>')
- for session in sessions:
- w(u'<li>%s (%s: %s)<br/>' % (
- xml_escape(text_type(session)),
- _('last usage'),
- strftime(dtformat, localtime(session.timestamp))))
- dict_to_html(w, session.data)
- w(u'</li>')
- w(u'</ul>')
- else:
- w(u'<p>%s</p>' % _('no repository sessions found'))
# web server information
w(u'<h2>%s</h2>' % _('Web server'))
pyvalue = ((_('base url'), req.base_url()),
--- a/cubicweb/web/views/editcontroller.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/editcontroller.py Mon Mar 20 10:28:01 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 <http://www.gnu.org/licenses/>.
"""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()
--- a/cubicweb/web/views/editforms.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/editforms.py Mon Mar 20 10:28:01 2017 +0100
@@ -71,11 +71,25 @@
# don't use navigation, all entities asked to be deleted should be displayed
# else we will only delete the displayed page
paginable = False
+ # show first level of composite relations in a treeview
+ show_composite = False
+ show_composite_skip_rtypes = set('wf_info_for',)
+
+ def _iter_composite_entities(self, entity, limit=None):
+ for rdef, role in entity.e_schema.composite_rdef_roles:
+ if rdef.rtype in self.show_composite_skip_rtypes:
+ continue
+ for centity in entity.related(
+ rdef.rtype, role, limit=limit
+ ).entities():
+ yield centity
def call(self, onsubmit=None):
"""ask for confirmation before real deletion"""
req, w = self._cw, self.w
_ = req._
+ if self.show_composite:
+ req.add_css(('jquery-treeview/jquery.treeview.css', 'cubicweb.treeview.css'))
w(u'<script type="text/javascript">updateMessage(\'%s\');</script>\n'
% _('this action is not reversible!'))
# XXX above message should have style of a warning
@@ -84,11 +98,30 @@
rset=self.cw_rset,
onsubmit=onsubmit)
w(u'<ul>\n')
+ page_size = req.property_value('navigation.page-size')
for entity in self.cw_rset.entities():
# don't use outofcontext view or any other that may contain inline
# edition form
- w(u'<li>%s</li>' % tags.a(entity.view('textoutofcontext'),
- href=entity.absolute_url()))
+ w(u'<li>%s' % tags.a(entity.view('textoutofcontext'),
+ href=entity.absolute_url()))
+ if self.show_composite:
+ content = None
+ for count, centity in enumerate(self._iter_composite_entities(
+ entity, limit=page_size,
+ )):
+ if count == 0:
+ w(u'<ul class="treeview">')
+ if content is not None:
+ w(u'<li>%s</li>' % content)
+ if count == page_size - 1:
+ w(u'<li class="last">%s</li></ul>' % _(
+ 'And more composite entities'))
+ break
+ content = tags.a(centity.view('textoutofcontext'),
+ href=centity.absolute_url())
+ else:
+ w(u'<li class="last">%s</li></ul>' % content)
+ w(u'</li>\n')
w(u'</ul>\n')
form.render(w=self.w)
--- a/cubicweb/web/views/forms.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/forms.py Mon Mar 20 10:28:01 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?
--- a/cubicweb/web/views/management.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/management.py Mon Mar 20 10:28:01 2017 +0100
@@ -188,7 +188,6 @@
def call(self):
stats = self._cw.call_service('repo_stats')
- stats['looping_tasks'] = ', '.join('%s (%s seconds)' % (n, i) for n, i in stats['looping_tasks'])
stats['threads'] = ', '.join(sorted(stats['threads']))
for k in stats:
if k in ('extid_cache_size', 'type_source_cache_size'):
--- a/cubicweb/web/views/sessions.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/sessions.py Mon Mar 20 10:28:01 2017 +0100
@@ -127,9 +127,6 @@
except InvalidSession:
self.close_session(session)
raise
- if session.closed:
- self.close_session(session)
- raise InvalidSession()
return session
def open_session(self, req):
@@ -176,4 +173,3 @@
"""
self.info('closing http session %s' % session.sessionid)
self._sessions.pop(session.sessionid, None)
- session.close()
--- a/cubicweb/web/views/staticcontrollers.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/staticcontrollers.py Mon Mar 20 10:28:01 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
--- a/cubicweb/web/views/uicfg.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/uicfg.py Mon Mar 20 10:28:01 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 <action>_on_new on relations. <action> 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):
--- a/cubicweb/web/views/workflow.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/views/workflow.py Mon Mar 20 10:28:01 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:
--- a/cubicweb/web/webconfig.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/web/webconfig.py Mon Mar 20 10:28:01 2017 +0100
@@ -78,11 +78,9 @@
))
-class WebConfiguration(CubicWebConfiguration):
- """the WebConfiguration is a singleton object handling instance's
- configuration and preferences
- """
- cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set([join('web', 'views')])
+class BaseWebConfiguration(CubicWebConfiguration):
+ """Base class for web configurations"""
+ cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set(['web.views'])
cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['views'])
options = merge_options(CubicWebConfiguration.options + (
@@ -92,7 +90,11 @@
'help': 'see `cubicweb.dbapi.connect` documentation for possible value',
'group': 'web', 'level': 2,
}),
-
+ ('use-uicache',
+ {'type': 'yn', 'default': True,
+ 'help': _('should css be compiled and store in uicache'),
+ 'group': 'ui', 'level': 2,
+ }),
('anonymous-user',
{'type' : 'string',
'default': None,
@@ -112,20 +114,41 @@
'help': 'web instance query log file',
'group': 'web', 'level': 3,
}),
+ ('cleanup-anonymous-session-time',
+ {'type' : 'time',
+ 'default': '5min',
+ 'help': 'Same as cleanup-session-time but specific to anonymous '
+ 'sessions. You can have a much smaller timeout here since it will be '
+ 'transparent to the user. Default to 5min.',
+ 'group': 'web', 'level': 3,
+ }),
+ ))
+
+ def anonymous_user(self):
+ """return a login and password to use for anonymous users.
+
+ None may be returned for both if anonymous connection is not
+ allowed or if an empty login is used in configuration
+ """
+ try:
+ user = self['anonymous-user'] or None
+ passwd = self['anonymous-password']
+ if user:
+ user = text_type(user)
+ except KeyError:
+ user, passwd = None, None
+ except UnicodeDecodeError:
+ raise ConfigurationError("anonymous information should only contains ascii")
+ return user, passwd
+
+
+
+class WebConfiguration(BaseWebConfiguration):
+ """the WebConfiguration is a singleton object handling instance's
+ configuration and preferences
+ """
+ options = merge_options(BaseWebConfiguration.options + (
# 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/". '
@@ -154,14 +177,6 @@
"Should be 0 or greater than repository\'s session-time.",
'group': 'web', 'level': 2,
}),
- ('cleanup-anonymous-session-time',
- {'type' : 'time',
- 'default': '5min',
- 'help': 'Same as cleanup-session-time but specific to anonymous '
- 'sessions. You can have a much smaller timeout here since it will be '
- 'transparent to the user. Default to 5min.',
- 'group': 'web', 'level': 3,
- }),
('submit-mail',
{'type' : 'string',
'default': None,
@@ -261,9 +276,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:
@@ -280,23 +293,6 @@
def vc_config(self):
return self.repository().get_versions()
- def anonymous_user(self):
- """return a login and password to use for anonymous users.
-
- None may be returned for both if anonymous connection is not
- allowed or if an empty login is used in configuration
- """
- try:
- user = self['anonymous-user'] or None
- passwd = self['anonymous-password']
- if user:
- user = text_type(user)
- except KeyError:
- user, passwd = None, None
- except UnicodeDecodeError:
- raise ConfigurationError("anonymous information should only contains ascii")
- return user, passwd
-
@cachedproperty
def _instance_salt(self):
"""This random key/salt is used to sign content to be sent back by
@@ -343,7 +339,7 @@
directory = self._fs_path_locate(rid, rdirectory)
if directory is None:
return None, None
- if rdirectory == 'data' and rid.endswith('.css'):
+ if self['use-uicache'] and rdirectory == 'data' and rid.endswith('.css'):
if rid == 'cubicweb.old.css':
# @import('cubicweb.css') in css
warn('[3.20] cubicweb.old.css has been renamed back to cubicweb.css',
@@ -382,16 +378,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 +397,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')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/wfutils.py Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,149 @@
+# 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 <http://www.gnu.org/licenses/>.
+"""Workflow setup utilities.
+
+These functions work with a declarative workflow definition:
+
+.. code-block:: python
+
+ {
+ 'etypes': 'CWGroup',
+ 'default': True,
+ 'initial_state': u'draft',
+ 'states': [u'draft', u'published'],
+ 'transitions': {
+ u'publish': {
+ 'fromstates': u'draft',
+ 'tostate': u'published',
+ 'requiredgroups': u'managers'
+ 'conditions': (
+ 'U in_group X',
+ 'X owned_by U'
+ )
+ }
+ }
+ }
+
+.. autofunction:: setup_workflow
+.. autofunction:: cleanupworkflow
+"""
+
+import collections
+
+from six import text_type
+
+from cubicweb import NoResultError
+
+
+def get_tuple_or_list(value):
+ if value is None:
+ return None
+ if not isinstance(value, (tuple, list)):
+ value = (value,)
+ return value
+
+
+def cleanupworkflow(cnx, wf, wfdef):
+ """Cleanup an existing workflow by removing the states and transitions that
+ do not exist in the given definition.
+
+ :param cnx: A connexion with enough permissions to define a workflow
+ :param wf: A `Workflow` entity
+ :param wfdef: A workflow definition
+ """
+ cnx.execute(
+ 'DELETE State S WHERE S state_of WF, WF eid %%(wf)s, '
+ 'NOT S name IN (%s)' % (
+ ', '.join('"%s"' % s for s in wfdef['states'])),
+ {'wf': wf.eid})
+
+ cnx.execute(
+ 'DELETE Transition T WHERE T transition_of WF, WF eid %%(wf)s, '
+ 'NOT T name IN (%s)' % (
+ ', '.join('"%s"' % s for s in wfdef['transitions'])),
+ {'wf': wf.eid})
+
+
+def setup_workflow(cnx, name, wfdef, cleanup=True):
+ """Create or update a workflow definition so it matches the given
+ definition.
+
+ :param cnx: A connexion with enough permissions to define a workflow
+ :param name: The workflow name. Used to create the `Workflow` entity, or
+ to find an existing one.
+ :param wfdef: A workflow definition.
+ :param cleanup: Remove extra states and transitions. Can be done separatly
+ by calling :func:`cleanupworkflow`.
+ :return: The created/updated workflow entity
+ """
+ name = text_type(name)
+ try:
+ wf = cnx.find('Workflow', name=name).one()
+ except NoResultError:
+ wf = cnx.create_entity('Workflow', name=name)
+
+ etypes = get_tuple_or_list(wfdef['etypes'])
+ cnx.execute('DELETE WF workflow_of ETYPE WHERE WF eid %%(wf)s, '
+ 'NOT ETYPE name IN (%s)' % ','.join('"%s"' for e in etypes),
+ {'wf': wf.eid})
+ cnx.execute('SET WF workflow_of ETYPE WHERE'
+ ' NOT WF workflow_of ETYPE, WF eid %%(wf)s, ETYPE name IN (%s)'
+ % ','.join('"%s"' % e for e in etypes),
+ {'wf': wf.eid})
+ if wfdef['default']:
+ cnx.execute(
+ 'SET ETYPE default_workflow X '
+ 'WHERE '
+ 'NOT ETYPE default_workflow X, '
+ 'X eid %%(x)s, ETYPE name IN (%s)' % ','.join(
+ '"%s"' % e for e in etypes),
+ {'x': wf.eid})
+
+ states = {}
+ states_transitions = collections.defaultdict(list)
+ for state in wfdef['states']:
+ st = wf.state_by_name(state) or wf.add_state(state)
+ states[state] = st
+
+ if 'initial_state' in wfdef:
+ wf.cw_set(initial_state=states[wfdef['initial_state']])
+
+ for trname, trdef in wfdef['transitions'].items():
+ tr = (wf.transition_by_name(trname) or
+ cnx.create_entity('Transition', name=trname))
+ tr.cw_set(transition_of=wf)
+ if trdef.get('tostate'):
+ tr.cw_set(destination_state=states[trdef['tostate']])
+ fromstates = get_tuple_or_list(trdef.get('fromstates', ()))
+ for stname in fromstates:
+ states_transitions[stname].append(tr)
+
+ requiredgroups = get_tuple_or_list(trdef.get('requiredgroups', ()))
+ conditions = get_tuple_or_list(trdef.get('conditions', ()))
+
+ tr.set_permissions(requiredgroups, conditions, reset=True)
+
+ for stname, transitions in states_transitions.items():
+ state = states[stname]
+ state.cw_set(allowed_transition=None)
+ state.cw_set(allowed_transition=transitions)
+
+ if cleanup:
+ cleanupworkflow(cnx, wf, wfdef)
+
+ return wf
--- a/cubicweb/wsgi/request.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/wsgi/request.py Mon Mar 20 10:28:01 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', ''))
--- a/cubicweb/wsgi/server.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/wsgi/server.py Mon Mar 20 10:28:01 2017 +0100
@@ -39,7 +39,6 @@
httpd.set_app(app)
repo = app.appli.repo
try:
- repo.start_looping_tasks()
LOGGER.info('starting http server on %s', config['base-url'])
httpd.serve_forever()
finally:
--- a/cubicweb/wsgi/test/unittest_wsgi.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/wsgi/test/unittest_wsgi.py Mon Mar 20 10:28:01 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('/', {
--- a/cubicweb/wsgi/tnd.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/wsgi/tnd.py Mon Mar 20 10:28:01 2017 +0100
@@ -40,7 +40,6 @@
http_server.listen(port, interface)
repo = app.appli.repo
try:
- repo.start_looping_tasks()
LOGGER.info('starting http server on %s', config['base-url'])
ioloop.IOLoop.instance().start()
finally:
--- a/cubicweb/wsgi/wz.py Mon Mar 20 09:40:24 2017 +0100
+++ b/cubicweb/wsgi/wz.py Mon Mar 20 10:28:01 2017 +0100
@@ -38,7 +38,6 @@
app = CubicWebWSGIApplication(config)
repo = app.appli.repo
try:
- repo.start_looping_tasks()
LOGGER.info('starting http server on %s', config['base-url'])
run_simple(interface, port, app,
threaded=True,
--- a/debian/control Mon Mar 20 09:40:24 2017 +0100
+++ b/debian/control Mon Mar 20 10:28:01 2017 +0100
@@ -25,7 +25,8 @@
python-pyramid,
python-pyramid-multiauth,
python-waitress,
- python-passlib (<< 2.0),
+ python-passlib (>= 1.7.0),
+ python-repoze.lru,
python-wsgicors,
sphinx-common,
Standards-Version: 3.9.6
@@ -157,6 +158,7 @@
python-pyramid-multiauth,
python-waitress (>= 0.8.9),
python-wsgicors,
+ python-repoze.lru,
Recommends:
python-pyramid-debugtoolbar
Conflicts:
--- a/doc/api/pyramid/authplugin.rst Mon Mar 20 09:40:24 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:
--- a/doc/api/pyramid/tools.rst Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/api/pyramid/tools.rst Mon Mar 20 10:28:01 2017 +0100
@@ -1,7 +1,7 @@
.. _tools_module:
:mod:`cubicweb.pyramid.tools`
-----------------------------
+-----------------------------
.. automodule:: cubicweb.pyramid.tools
--- a/doc/book/admin/instance-config.rst Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/book/admin/instance-config.rst Mon Mar 20 10:28:01 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
-------------------------
--- a/doc/book/devrepo/datamodel/define-workflows.rst Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/book/devrepo/datamodel/define-workflows.rst Mon Mar 20 10:28:01 2017 +0100
@@ -158,3 +158,11 @@
re-create all the workflow entities. The user interface should only be
a reference for you to view the states and transitions, but is not the
appropriate interface to define your application workflow.
+
+
+Alternative way to declare workflows
+------------------------------------
+
+.. automodule:: cubicweb.wfutils
+
+
--- a/doc/book/devweb/edition/dissection.rst Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/book/devweb/edition/dissection.rst Mon Mar 20 10:28:01 2017 +0100
@@ -107,14 +107,14 @@
<th class="labelCol"><label class="required" for="title-subject:763">title</label></th>
<td>
<input id="title-subject:763" maxlength="128" name="title-subject:763" size="45"
- tabindex="1" type="text" value="let us write more doc" />
+ type="text" value="let us write more doc" />
</td>
</tr>
... (description field omitted) ...
<tr class="priority_subject_row">
<th class="labelCol"><label class="required" for="priority-subject:763">priority</label></th>
<td>
- <select id="priority-subject:763" name="priority-subject:763" size="1" tabindex="4">
+ <select id="priority-subject:763" name="priority-subject:763" size="1">
<option value="important">important</option>
<option selected="selected" value="normal">normal</option>
<option value="minor">minor</option>
@@ -126,7 +126,7 @@
<tr class="concerns_subject_row">
<th class="labelCol"><label class="required" for="concerns-subject:763">concerns</label></th>
<td>
- <select id="concerns-subject:763" name="concerns-subject:763" size="1" tabindex="6">
+ <select id="concerns-subject:763" name="concerns-subject:763" size="1">
<option selected="selected" value="760">Foo</option>
</select>
</td>
@@ -134,7 +134,7 @@
<tr class="done_in_subject_row">
<th class="labelCol"><label for="done_in-subject:763">done in</label></th>
<td>
- <select id="done_in-subject:763" name="done_in-subject:763" size="1" tabindex="7">
+ <select id="done_in-subject:763" name="done_in-subject:763" size="1">
<option value="__cubicweb_internal_field__"></option>
<option selected="selected" value="761">Foo 0.1.0</option>
<option value="762">Foo 0.2.0</option>
@@ -180,7 +180,7 @@
<tr><th> </th><td> </td></tr>
<tr id="relationSelectorRow_763" class="separator">
<th class="labelCol">
- <select id="relationSelector_763" tabindex="8"
+ <select id="relationSelector_763"
onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,763);">
<option value="">select a relation</option>
<option value="appeared_in_subject">appeared in</option>
@@ -228,7 +228,7 @@
<tbody>
<tr>
<td align="center">
- <button class="validateButton" tabindex="9" type="submit" value="validate">
+ <button class="validateButton" type="submit" value="validate">
<img alt="OK_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/ok.png" />
validate
</button>
@@ -236,13 +236,13 @@
<td style="align: right; width: 50%;">
<button class="validateButton"
onclick="postForm('__action_apply', 'button_apply', 'entityForm')"
- tabindex="10" type="button" value="apply">
+ type="button" value="apply">
<img alt="APPLY_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/plus.png" />
apply
</button>
<button class="validateButton"
onclick="postForm('__action_cancel', 'button_cancel', 'entityForm')"
- tabindex="11" type="button" value="cancel">
+ type="button" value="cancel">
<img alt="CANCEL_ICON" src="http://myapp/datafd8b5d92771209ede1018a8d5da46a37/cancel.png" />
cancel
</button>
--- a/doc/book/devweb/request.rst Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/book/devweb/request.rst Mon Mar 20 10:28:01 2017 +0100
@@ -48,8 +48,6 @@
* etype_rset
* `form`, dictionary containing the values of a web form
* `encoding`, character encoding to use in the response
- * `next_tabindex()`: returns a monotonically growing integer used to
- build the html tab index of forms
* `HTTP`
--- a/doc/book/pyramid/quickstart.rst Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/book/pyramid/quickstart.rst Mon Mar 20 10:28:01 2017 +0100
@@ -57,3 +57,17 @@
- Configure the base-url and https-url in all-in-one.conf to match the ones
of the pyramid configuration (this is a temporary limitation).
+
+
+Usage with pserve
+-----------------
+
+To run a Pyramid application using pserve_:
+
+::
+
+ pserve /path/to/development.ini instance=<appid>
+
+
+.. _pserve: \
+ http://docs.pylonsproject.org/projects/pyramid/en/latest/pscripts/pserve.html
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.25.rst Mon Mar 20 10:28:01 2017 +0100
@@ -0,0 +1,16 @@
+3.25 (UNRELEASED)
+=================
+
+New features
+------------
+
+* A new option `connections-pooler-enabled` (default yes) has been added. This
+ allow to switch off internal connection pooling for use with others poolers
+ such as pgbouncer_.
+
+.. _pgbouncer: https://pgbouncer.github.io/
+
+
+* A new way to declare workflows as simple data structure (dict/list) has been
+ introduced. Respective utility functions live in ``cubicweb.wfutils``
+ module. This handles both the creation and migration of workflows.
--- a/doc/changes/changelog.rst Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/changes/changelog.rst Mon Mar 20 10:28:01 2017 +0100
@@ -2,6 +2,7 @@
Changelog history
===================
+.. include:: 3.25.rst
.. include:: 3.24.rst
.. include:: 3.23.rst
.. include:: 3.22.rst
--- a/doc/changes/index.rst Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/changes/index.rst Mon Mar 20 10:28:01 2017 +0100
@@ -4,6 +4,7 @@
.. toctree::
:maxdepth: 1
+ 3.25
3.24
3.23
3.22
--- a/doc/index.rst Mon Mar 20 09:40:24 2017 +0100
+++ b/doc/index.rst Mon Mar 20 10:28:01 2017 +0100
@@ -108,7 +108,7 @@
.. toctree::
:maxdepth: 1
- js_api/index
+ book/en/devweb/js_api/index
Developpers
~~~~~~~~~~~
--- a/flake8-ok-files.txt Mon Mar 20 09:40:24 2017 +0100
+++ b/flake8-ok-files.txt Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,5 @@
+cubicweb/__init__.py
+cubicweb/__main__.py
cubicweb/dataimport/csv.py
cubicweb/dataimport/importer.py
cubicweb/dataimport/massive_store.py
@@ -18,24 +20,31 @@
cubicweb/devtools/test/unittest_webtest.py
cubicweb/devtools/webtest.py
cubicweb/entities/adapters.py
+cubicweb/entities/authobjs.py
cubicweb/entities/test/unittest_base.py
cubicweb/etwist/__init__.py
+cubicweb/etwist/request.py
+cubicweb/etwist/service.py
cubicweb/ext/__init__.py
cubicweb/hooks/test/data/hooks.py
-cubicweb/hooks/test/unittest_notification.py
+cubicweb/hooks/test/unittest_notificationhooks.py
cubicweb/hooks/test/unittest_security.py
cubicweb/hooks/test/unittest_syncsession.py
-cubicweb/__init__.py
-cubicweb/__main__.py
cubicweb/pylintext.py
+cubicweb/repoapi.py
+cubicweb/rset.py
+cubicweb/rtags.py
+cubicweb/server/__init__.py
cubicweb/server/repository.py
cubicweb/server/rqlannotation.py
cubicweb/server/schema2sql.py
cubicweb/server/session.py
cubicweb/server/sqlutils.py
+cubicweb/server/utils.py
cubicweb/server/test/datacomputed/migratedapp/schema.py
cubicweb/server/test/datacomputed/schema.py
cubicweb/server/test/data/entities.py
+cubicweb/server/test/data/hooks.py
cubicweb/server/test/data-migractions/cubes/fakecustomtype/__init__.py
cubicweb/server/test/data-migractions/cubes/fakeemail/__init__.py
cubicweb/server/test/data-migractions/cubes/__init__.py
@@ -43,27 +52,31 @@
cubicweb/server/test/data-schema2sql/__init__.py
cubicweb/server/test/unittest_checkintegrity.py
cubicweb/server/test/unittest_ldapsource.py
+cubicweb/server/test/unittest_serverctl.py
+cubicweb/server/test/unittest_session.py
cubicweb/server/test/unittest_rqlannotation.py
-cubicweb/skeleton/test/pytestconf.py
+cubicweb/server/test/unittest_utils.py
+cubicweb/sobjects/notification.py
+cubicweb/sobjects/services.py
cubicweb/sobjects/test/unittest_notification.py
cubicweb/sobjects/test/unittest_register_user.py
cubicweb/sobjects/textparsers.py
-cubicweb/test/data/cubes/comment/__init__.py
-cubicweb/test/data/cubes/comment/__pkginfo__.py
-cubicweb/test/data/cubes/email/entities.py
-cubicweb/test/data/cubes/email/hooks.py
-cubicweb/test/data/cubes/email/__init__.py
-cubicweb/test/data/cubes/email/__pkginfo__.py
-cubicweb/test/data/cubes/email/views/__init__.py
-cubicweb/test/data/cubes/file/entities/__init__.py
-cubicweb/test/data/cubes/file/hooks/__init__.py
-cubicweb/test/data/cubes/file/__init__.py
-cubicweb/test/data/cubes/file/__pkginfo__.py
-cubicweb/test/data/cubes/file/views.py
-cubicweb/test/data/cubes/forge/__init__.py
-cubicweb/test/data/cubes/forge/__pkginfo__.py
-cubicweb/test/data/cubes/mycube/__init__.py
-cubicweb/test/data/cubes/mycube/__pkginfo__.py
+cubicweb/test/data/libpython/cubicweb_comment/__init__.py
+cubicweb/test/data/libpython/cubicweb_comment/__pkginfo__.py
+cubicweb/test/data/libpython/cubicweb_email/entities.py
+cubicweb/test/data/libpython/cubicweb_email/hooks.py
+cubicweb/test/data/libpython/cubicweb_email/__init__.py
+cubicweb/test/data/libpython/cubicweb_email/__pkginfo__.py
+cubicweb/test/data/libpython/cubicweb_email/views/__init__.py
+cubicweb/test/data/libpython/cubicweb_file/entities/__init__.py
+cubicweb/test/data/libpython/cubicweb_file/hooks/__init__.py
+cubicweb/test/data/libpython/cubicweb_file/__init__.py
+cubicweb/test/data/libpython/cubicweb_file/__pkginfo__.py
+cubicweb/test/data/libpython/cubicweb_file/views.py
+cubicweb/test/data/libpython/cubicweb_forge/__init__.py
+cubicweb/test/data/libpython/cubicweb_forge/__pkginfo__.py
+cubicweb/test/data/libpython/cubicweb_mycube/__init__.py
+cubicweb/test/data/libpython/cubicweb_mycube/__pkginfo__.py
cubicweb/test/data/migration/0.1.0_common.py
cubicweb/test/data/migration/0.1.0_repository.py
cubicweb/test/data_schemareader/schema.py
@@ -72,23 +85,31 @@
cubicweb/test/unittest_binary.py
cubicweb/test/unittest_mail.py
cubicweb/test/unittest_repoapi.py
+cubicweb/test/unittest_req.py
+cubicweb/test/unittest_rtags.py
cubicweb/test/unittest_schema.py
cubicweb/test/unittest_toolsutils.py
-cubicweb/test/unittest_utils.py
+cubicweb/test/unittest_wfutils.py
+cubicweb/web/application.py
cubicweb/web/formwidgets.py
cubicweb/web/test/data/entities.py
+cubicweb/web/test/unittest_application.py
cubicweb/web/test/unittest_http_headers.py
cubicweb/web/test/unittest_views_basetemplates.py
cubicweb/web/test/unittest_views_cwsources.py
cubicweb/web/test/unittest_views_json.py
+cubicweb/web/views/authentication.py
+cubicweb/web/views/editcontroller.py
cubicweb/web/views/json.py
cubicweb/web/views/searchrestriction.py
+cubicweb/web/views/staticcontrollers.py
+cubicweb/web/views/uicfg.py
cubicweb/xy.py
cubicweb/pyramid/auth.py
cubicweb/pyramid/bwcompat.py
+cubicweb/pyramid/config.py
cubicweb/pyramid/core.py
cubicweb/pyramid/defaults.py
-cubicweb/pyramid/init_instance.py
cubicweb/pyramid/__init__.py
cubicweb/pyramid/login.py
cubicweb/pyramid/predicates.py
@@ -99,8 +120,10 @@
cubicweb/pyramid/tools.py
cubicweb/pyramid/test/__init__.py
cubicweb/pyramid/test/test_bw_request.py
+cubicweb/pyramid/test/test_config.py
cubicweb/pyramid/test/test_core.py
cubicweb/pyramid/test/test_login.py
cubicweb/pyramid/test/test_rest_api.py
cubicweb/pyramid/test/test_tools.py
cubicweb/pyramid/pyramidctl.py
+cubicweb/wfutils.py
--- a/requirements/dev.txt Mon Mar 20 09:40:24 2017 +0100
+++ b/requirements/dev.txt Mon Mar 20 10:28:01 2017 +0100
@@ -1,1 +1,3 @@
pytest
+http://hg.logilab.org/master/logilab/common/archive/default.tar.bz2#egg=logilab-common
+http://hg.logilab.org/master/yams/archive/default.tar.bz2#egg=yams
--- a/requirements/test-server.txt Mon Mar 20 09:40:24 2017 +0100
+++ b/requirements/test-server.txt Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,4 @@
+mock
psycopg2
ldap3 < 2
cubicweb-basket
--- a/setup.cfg Mon Mar 20 09:40:24 2017 +0100
+++ b/setup.cfg Mon Mar 20 10:28:01 2017 +0100
@@ -1,3 +1,6 @@
+[bdist_wheel]
+universal = 1
+
[check-manifest]
ignore =
debian
--- a/setup.py Mon Mar 20 09:40:24 2017 +0100
+++ b/setup.py Mon Mar 20 10:28:01 2017 +0100
@@ -149,40 +149,16 @@
dest = join(self.install_dir, src)
export(src, dest, verbose=self.verbose)
-# write required share/cubicweb/cubes/__init__.py
-class MyInstallData(install_data.install_data):
- """A class That manages data files installation"""
- def run(self):
- """overridden from install_data class"""
- install_data.install_data.run(self)
- path = join(self.install_dir, 'share', 'cubicweb', 'cubes', '__init__.py')
- ini = open(path, 'w')
- ini.write('# Cubicweb cubes directory\n')
- ini.close()
-
-
-class CWDevelop(develop.develop):
- """Custom "develop" command warning about (legacy) cubes directory not
- installed.
- """
-
- def run(self):
- cubespath = join(sys.prefix, 'share', 'cubicweb', 'cubes')
- self.warn('develop command does not install (legacy) cubes directory (%s)'
- % cubespath)
- return develop.develop.run(self)
-
# re-enable copying data files in sys.prefix
-# overwrite MyInstallData to use sys.prefix instead of the egg directory
-MyInstallMoreData = MyInstallData
-class MyInstallData(MyInstallMoreData): # pylint: disable=E0102
+# overwrite install_data to use sys.prefix instead of the egg directory
+class MyInstallData(install_data.install_data):
"""A class that manages data files installation"""
def run(self):
_old_install_dir = self.install_dir
if self.install_dir.endswith('egg'):
self.install_dir = sys.prefix
- MyInstallMoreData.run(self)
+ install_data.install_data.run(self)
self.install_dir = _old_install_dir
try:
import setuptools.command.easy_install # only if easy_install available
@@ -223,11 +199,16 @@
'yams >= 0.44.0',
'lxml',
'logilab-database >= 1.15.0',
- 'passlib < 2.0',
+ 'passlib >= 1.7.0',
'pytz',
'Markdown',
'unittest2 >= 0.7.0',
],
+ entry_points={
+ 'paste.app_factory': [
+ 'main=cubicweb.pyramid:pyramid_app',
+ ],
+ },
extras_require={
'captcha': [
'Pillow',
@@ -249,6 +230,7 @@
'waitress >= 0.8.9',
'wsgicors >= 0.3',
'pyramid_multiauth',
+ 'repoze.lru',
],
'rdf': [
'rdflib',
@@ -263,7 +245,6 @@
cmdclass={
'install_lib': MyInstallLib,
'install_data': MyInstallData,
- 'develop': CWDevelop,
},
zip_safe=False,
)
--- a/tox.ini Mon Mar 20 09:40:24 2017 +0100
+++ b/tox.ini Mon Mar 20 10:28:01 2017 +0100
@@ -5,8 +5,6 @@
[testenv]
sitepackages = True
-whitelist_externals =
- /usr/bin/touch
deps =
-r{toxinidir}/requirements/dev.txt
py27: backports.tempfile
@@ -14,7 +12,6 @@
server: -r{toxinidir}/requirements/test-server.txt
web: -r{toxinidir}/requirements/test-web.txt
commands =
- py34: touch {envdir}/share/cubicweb/cubes/__init__.py
misc: {envpython} -m pip install --upgrade --no-deps --quiet git+git://github.com/logilab/yapps@master#egg=yapps
misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/test {toxinidir}/cubicweb/dataimport/test {toxinidir}/cubicweb/devtools/test {toxinidir}/cubicweb/entities/test {toxinidir}/cubicweb/ext/test {toxinidir}/cubicweb/hooks/test {toxinidir}/cubicweb/sobjects/test {toxinidir}/cubicweb/wsgi/test {toxinidir}/cubicweb/pyramid/test
py27-misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/etwist/test
@@ -53,7 +50,7 @@
format = pylint
ignore = W503
max-line-length = 100
-exclude = setup.py,doc/*,cubicweb/misc/*,cubicweb/test/*,cubicweb/*/test/*,.tox/*
+exclude = doc/*,.tox/*
# vim: wrap sts=2 sw=2