--- a/.hgtags Wed Feb 22 11:57:42 2012 +0100
+++ b/.hgtags Tue Oct 23 15:00:53 2012 +0200
@@ -245,3 +245,24 @@
4d0f5d18e8a07ab218efe90d758af723ea4a1b2b cubicweb-debian-version-3.14.3-1
508645a542870cb0def9c43056e5084ff8def5ca cubicweb-version-3.14.4
bc40991b7f13642d457f5ca80ac1486c29e25a6e cubicweb-debian-version-3.14.4-1
+4c8cb2e9d0ee13af1d584e2920d1ae76f47380e9 cubicweb-debian-version-3.14.4-2
+f559ab9602e7eeb4996ac0f83d544a6e0374e204 cubicweb-version-3.14.5
+55fc796ed5d5f31245ae60bd148c9e42657a1af6 cubicweb-debian-version-3.14.5-1
+db021578232b885dc5e55dfca045332ce01e7f35 cubicweb-version-3.14.6
+75364c0994907764715bd5011f6a59d934dbeb7d cubicweb-debian-version-3.14.6-1
+0642b2d03acaa5e065cae7590e82b388a280ca22 cubicweb-version-3.15.0
+925db25a3250c5090cf640fc2b02bde5818b9798 cubicweb-debian-version-3.15.0-1
+3ba3ee5b3a89a54d1dc12ed41d5c12232eda1952 cubicweb-version-3.14.7
+20ee573bd2379a00f29ff27bb88a8a3344d4cdfe cubicweb-debian-version-3.14.7-1
+15fe07ff687238f8cc09d8e563a72981484085b3 cubicweb-version-3.14.8
+81394043ad226942ac0019b8e1d4f7058d67a49f cubicweb-debian-version-3.14.8-1
+9337812cef6b949eee89161190e0c3d68d7f32ea cubicweb-version-3.14.9
+68c762adf2d5a2c338910ef1091df554370586f0 cubicweb-debian-version-3.14.9-1
+783a5df54dc742e63c8a720b1582ff08366733bd cubicweb-version-3.15.1
+fe5e60862b64f1beed2ccdf3a9c96502dfcd811b cubicweb-debian-version-3.15.1-1
+2afc157ea9b2b92eccb0f2d704094e22ce8b5a05 cubicweb-version-3.15.2
+9aa5553b26520ceb68539e7a32721b5cd5393e16 cubicweb-debian-version-3.15.2-1
+0e012eb80990ca6f91aa9a8ad3324fbcf51435b1 cubicweb-version-3.15.3
+7ad423a5b6a883dbdf00e6c87a5f8ab121041640 cubicweb-debian-version-3.15.3-1
+63260486de89a9dc32128cd0eacef891a668977b cubicweb-version-3.15.4
+70cb36c826df86de465f9b69647cef7096dcf12c cubicweb-debian-version-3.15.4-1
--- a/MANIFEST.in Wed Feb 22 11:57:42 2012 +0100
+++ b/MANIFEST.in Tue Oct 23 15:00:53 2012 +0200
@@ -12,7 +12,7 @@
include web/views/*.pt
recursive-include web/data external_resources *.js *.css *.py *.png *.gif *.ico *.ttf
recursive-include web/wdoc *.rst *.png *.xml ChangeLog*
-recursive-include devtools/data *.js *.css
+recursive-include devtools/data *.js *.css *.sh
recursive-include i18n *.pot *.po
recursive-include schemas *.py *.sql
--- a/__init__.py Wed Feb 22 11:57:42 2012 +0100
+++ b/__init__.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -41,6 +41,7 @@
from StringIO import StringIO
from logilab.common.logging_ext import set_log_methods
+from yams.constraints import BASE_CONVERTERS
if os.environ.get('APYCOT_ROOT'):
@@ -120,6 +121,13 @@
binary.seek(0)
return binary
+def str_or_binary(value):
+ if isinstance(value, Binary):
+ return value
+ return str(value)
+BASE_CONVERTERS['Password'] = str_or_binary
+
+
# use this dictionary to rename entity types while keeping bw compat
ETYPE_NAME_MAP = {}
@@ -191,3 +199,26 @@
CW_EVENT_MANAGER.bind(event, func, *args, **kwargs)
return func
return _decorator
+
+
+from yams.schema import role_name as rname
+
+def validation_error(entity, errors, substitutions=None, i18nvalues=None):
+ """easy way to retrieve a :class:`cubicweb.ValidationError` for an entity or eid.
+
+ You may also have 2-tuple as error keys, :func:`yams.role_name` will be
+ called automatically for them.
+
+ Messages in errors **should not be translated yet**, though marked for
+ internationalization. You may give an additional substition dictionary that
+ will be used for interpolation after the translation.
+ """
+ if substitutions is None:
+ # set empty dict else translation won't be done for backward
+ # compatibility reason (see ValidationError.tr method)
+ substitutions = {}
+ for key in errors.keys():
+ if isinstance(key, tuple):
+ errors[rname(*key)] = errors.pop(key)
+ return ValidationError(getattr(entity, 'eid', entity), errors,
+ substitutions, i18nvalues)
--- a/__pkginfo__.py Wed Feb 22 11:57:42 2012 +0100
+++ b/__pkginfo__.py Tue Oct 23 15:00:53 2012 +0200
@@ -22,7 +22,7 @@
modname = distname = "cubicweb"
-numversion = (3, 15, 0)
+numversion = (3, 15, 4)
version = '.'.join(str(num) for num in numversion)
description = "a repository of entities / relations for knowledge management"
@@ -42,9 +42,8 @@
__depends__ = {
'logilab-common': '>= 0.58.0',
'logilab-mtconverter': '>= 0.8.0',
- 'rql': '>= 0.28.0',
- 'yams': '>= 0.34.0',
- 'docutils': '>= 0.6',
+ 'rql': '>= 0.31.2',
+ 'yams': '>= 0.36.0',
#gettext # for xgettext, msgcat, etc...
# web dependancies
'simplejson': '>= 2.0.9',
@@ -54,9 +53,11 @@
# server dependencies
'logilab-database': '>= 1.8.2',
'pysqlite': '>= 2.5.5', # XXX install pysqlite2
+ 'passlib': '',
}
__recommends__ = {
+ 'docutils': '>= 0.6',
'Pyro': '>= 3.9.1, < 4.0.0',
'PIL': '', # for captcha
'pycrypto': '', # for crypto extensions
--- a/_exceptions.py Wed Feb 22 11:57:42 2012 +0100
+++ b/_exceptions.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -19,7 +19,7 @@
__docformat__ = "restructuredtext en"
-from yams import ValidationError
+from yams import ValidationError as ValidationError
# abstract exceptions #########################################################
@@ -30,9 +30,10 @@
if self.msg:
if self.args:
return self.msg % tuple(self.args)
- return self.msg
- return ' '.join(unicode(arg) for arg in self.args)
-
+ else:
+ return self.msg
+ else:
+ return u' '.join(unicode(arg) for arg in self.args)
class ConfigurationError(CubicWebException):
"""a misconfiguration error"""
@@ -81,6 +82,7 @@
class UniqueTogetherError(RepositoryError):
"""raised when a unique_together constraint caused an IntegrityError"""
+
# security exceptions #########################################################
class Unauthorized(SecurityError):
@@ -128,6 +130,35 @@
a non final entity
"""
+class UndoTransactionException(QueryError):
+ """Raised when undoing a transaction could not be performed completely.
+
+ Note that :
+ 1) the partial undo operation might be acceptable
+ depending upon the final application
+
+ 2) the undo operation can also fail with a `ValidationError` in
+ cases where the undoing breaks integrity constraints checked
+ immediately.
+
+ 3) It might be that neither of those exception is raised but a
+ subsequent `commit` might raise a `ValidationError` in cases
+ where the undoing breaks integrity constraints checked at
+ commit time.
+
+ :type txuuix: int
+ :param txuuid: Unique identifier of the partialy undone transaction
+
+ :type errors: list
+ :param errors: List of errors occured during undoing
+ """
+ msg = u"The following error(s) occured while undoing transaction #%d : %s"
+
+ def __init__(self, txuuid, errors):
+ super(UndoTransactionException, self).__init__(txuuid, errors)
+ self.txuuid = txuuid
+ self.errors = errors
+
# tools exceptions ############################################################
class ExecutionError(Exception):
--- a/bin/clone_deps.py Wed Feb 22 11:57:42 2012 +0100
+++ b/bin/clone_deps.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,24 +1,25 @@
#!/usr/bin/python
-import os
import sys
-from subprocess import call, Popen, PIPE
-try:
- from mercurial.dispatch import dispatch as hg_call
-except ImportError:
+
+from subprocess import call as sbp_call, Popen, PIPE
+from urllib import urlopen
+import os
+from os import path as osp, pardir, chdir
+
+
+def find_mercurial():
+ print "trying to find mercurial from the command line ..."
print '-' * 20
- print "mercurial module is not reachable from this Python interpreter"
- print "trying from command line ..."
- tryhg = os.system('hg --version')
+ tryhg = sbp_call(['hg', '--version'])
if tryhg:
- print 'mercurial seems to unavailable, please install it'
+ print 'mercurial seems to be unavailable, please install it'
raise
- print 'found it, ok'
print '-' * 20
def hg_call(args):
- call(['hg'] + args)
-from urllib import urlopen
-from os import path as osp, pardir
-from os.path import normpath, join, dirname
+ return sbp_call(['hg'] + args)
+
+ return hg_call
+
BASE_URL = 'http://www.logilab.org/hg/'
@@ -27,7 +28,7 @@
'logilab/devtools', 'logilab/mtconverter',
'cubes/blog', 'cubes/calendar', 'cubes/card', 'cubes/comment',
'cubes/datafeed', 'cubes/email', 'cubes/file', 'cubes/folder',
- 'cubes/forgotpwd', 'cubes/keyword', 'cubes/link',
+ 'cubes/forgotpwd', 'cubes/keyword', 'cubes/link', 'cubes/localperms',
'cubes/mailinglist', 'cubes/nosylist', 'cubes/person',
'cubes/preview', 'cubes/registration', 'cubes/rememberme',
'cubes/tag', 'cubes/vcsfile', 'cubes/zone']
@@ -65,9 +66,10 @@
else:
sys.stderr.write('usage %s [base_url]\n' % sys.argv[0])
sys.exit(1)
+ hg_call = find_mercurial()
print len(to_clone), 'repositories will be cloned'
- base_dir = normpath(join(dirname(__file__), pardir, pardir))
- os.chdir(base_dir)
+ base_dir = osp.normpath(osp.join(osp.dirname(__file__), pardir, pardir))
+ chdir(base_dir)
not_updated = []
for repo in to_clone:
url = base_url + repo
@@ -78,7 +80,7 @@
directory, repo = repo.split('/')
if not osp.isdir(directory):
os.mkdir(directory)
- open(join(directory, '__init__.py'), 'w').close()
+ open(osp.join(directory, '__init__.py'), 'w').close()
target_path = osp.join(directory, repo)
if osp.exists(target_path):
print target_path, 'seems already cloned. Skipping it.'
--- a/cwconfig.py Wed Feb 22 11:57:42 2012 +0100
+++ b/cwconfig.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -171,6 +171,7 @@
import sys
import os
+import stat
import logging
import logging.config
from smtplib import SMTP
@@ -306,7 +307,10 @@
_forced_mode = os.environ.get('CW_MODE')
assert _forced_mode in (None, 'system', 'user')
-CWDEV = exists(join(CW_SOFTWARE_ROOT, '.hg'))
+#Â CWDEV tells whether directories such as i18n/, web/data/, etc. (ie containing
+# some other resources than python libraries) are located with the python code
+# or as a 'shared' cube
+CWDEV = exists(join(CW_SOFTWARE_ROOT, 'i18n'))
try:
_INSTALL_PREFIX = os.environ['CW_INSTALL_PREFIX']
@@ -386,14 +390,6 @@
'help': 'allow users to login with their primary email if set',
'group': 'main', 'level': 2,
}),
- ('use-request-subdomain',
- {'type' : 'yn',
- 'default': None,
- 'help': ('if set, base-url subdomain is replaced by the request\'s '
- 'host, to help managing sites with several subdomains in a '
- 'single cubicweb instance'),
- 'group': 'main', 'level': 1,
- }),
('mangle-emails',
{'type' : 'yn',
'default': False,
@@ -675,51 +671,6 @@
cubicweb_appobject_path = set(['entities'])
cube_appobject_path = set(['entities'])
- @classmethod
- def build_vregistry_path(cls, 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 = cls.build_vregistry_cubicweb_path(evobjpath)
- vregpath += cls.build_vregistry_cube_path(templpath, tvobjpath)
- return vregpath
-
- @classmethod
- def build_vregistry_cubicweb_path(cls, evobjpath=None):
- vregpath = []
- if evobjpath is None:
- evobjpath = cls.cubicweb_appobject_path
- for subdir in evobjpath:
- path = join(CW_SOFTWARE_ROOT, subdir)
- if exists(path):
- vregpath.append(path)
- return vregpath
-
- @classmethod
- def build_vregistry_cube_path(cls, templpath, tvobjpath=None):
- vregpath = []
- if tvobjpath is None:
- tvobjpath = cls.cube_appobject_path
- for directory in templpath:
- for subdir in tvobjpath:
- path = join(directory, subdir)
- if exists(path):
- vregpath.append(path)
- elif exists(path + '.py'):
- vregpath.append(path + '.py')
- return vregpath
-
def __init__(self, debugmode=False):
if debugmode:
# in python 2.7, DeprecationWarning are not shown anymore by default
@@ -767,12 +718,57 @@
# configure simpleTal logger
logging.getLogger('simpleTAL').setLevel(logging.ERROR)
- def vregistry_path(self):
+ 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.
"""
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, paths=None):
@@ -825,7 +821,7 @@
_cubes = None
def init_cubes(self, cubes):
- assert self._cubes is None, self._cubes
+ assert self._cubes is None, repr(self._cubes)
self._cubes = self.reorder_cubes(cubes)
# load cubes'__init__.py file first
for cube in cubes:
@@ -1078,7 +1074,12 @@
If not, try to fix this, letting exception propagate when not possible.
"""
if not exists(path):
- os.makedirs(path)
+ self.info('creating %s directory', path)
+ try:
+ os.makedirs(path)
+ except OSError, ex:
+ self.warning('error while creating %s directory: %s', path, ex)
+ return
if self['uid']:
try:
uid = int(self['uid'])
@@ -1092,10 +1093,20 @@
return
fstat = os.stat(path)
if fstat.st_uid != uid:
- os.chown(path, uid, os.getgid())
- import stat
+ self.info('giving ownership of %s directory to %s', path, self['uid'])
+ try:
+ os.chown(path, uid, os.getgid())
+ except OSError, ex:
+ self.warning('error while giving ownership of %s directory to %s: %s',
+ path, self['uid'], ex)
if not (fstat.st_mode & stat.S_IWUSR):
- os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+ self.info('forcing write permission on directory %s', path)
+ try:
+ os.chmod(path, fstat.st_mode | stat.S_IWUSR)
+ except OSError, ex:
+ self.warning('error while forcing write permission on directory %s: %s',
+ path, ex)
+ return
@cached
def instance_md5_version(self):
@@ -1157,17 +1168,20 @@
tr = translation('cubicweb', path, languages=[language])
self.translations[language] = (tr.ugettext, tr.upgettext)
except (ImportError, AttributeError, IOError):
- self.exception('localisation support error for language %s',
- language)
+ 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 vregistry_path(self):
+ 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_vregistry_path(templpath)
+ return self.build_appobjects_path(templpath)
def set_sources_mode(self, sources):
if not 'all' in sources:
--- a/cwctl.py Wed Feb 22 11:57:42 2012 +0100
+++ b/cwctl.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -38,6 +38,8 @@
from os.path import exists, join, isfile, isdir, dirname, abspath
+from urlparse import urlparse
+
from logilab.common.clcommands import CommandLine
from logilab.common.shellutils import ASK
@@ -867,31 +869,45 @@
'group': 'local'
}),
- ('pyro',
- {'short': 'P', 'action' : 'store_true',
- 'help': 'connect to a running instance through Pyro.',
- 'group': 'remote',
- }),
- ('pyro-ns-host',
- {'short': 'H', 'type' : 'string', 'metavar': '<host[:port]>',
- 'help': 'Pyro name server host. If not set, will be detected by '
- 'using a broadcast query.',
+ ('repo-uri',
+ {'short': 'H', 'type' : 'string', 'metavar': '<protocol>://<[host][:port]>',
+ 'help': 'URI of the CubicWeb repository to connect to. URI can be \
+pyro://[host:port] the Pyro name server host; if the pyro nameserver is not set, \
+it will be detected by using a broadcast query, a ZMQ URL or \
+inmemory:// (default) use an in-memory repository.',
'group': 'remote'
}),
)
def run(self, args):
appid = args.pop(0)
- if self.config.pyro:
+ if self.config.repo_uri:
+ uri = urlparse(self.config.repo_uri)
+ if uri.scheme == 'pyro':
+ cnxtype = uri.scheme
+ hostport = uri.netloc
+ elif uri.scheme == 'inmemory':
+ cnxtype = ''
+ hostport = ''
+ else:
+ cnxtype = 'zmq'
+ hostport = self.config.repo_uri
+ else:
+ cnxtype = ''
+
+ if cnxtype:
from cubicweb import AuthenticationError
- from cubicweb.dbapi import connect
+ from cubicweb.dbapi import connect, ConnectionProperties
from cubicweb.server.utils import manager_userpasswd
from cubicweb.server.migractions import ServerMigrationHelper
+ cnxprops = ConnectionProperties(cnxtype=cnxtype)
+
while True:
try:
login, pwd = manager_userpasswd(msg=None)
cnx = connect(appid, login=login, password=pwd,
- host=self.config.pyro_ns_host, mulcnx=False)
+ host=hostport, mulcnx=False,
+ cnxprops=cnxprops)
except AuthenticationError, ex:
print ex
except (KeyboardInterrupt, EOFError):
@@ -901,7 +917,7 @@
break
cnx.load_appobjects()
repo = cnx._repo
- mih = ServerMigrationHelper(None, repo=repo, cnx=cnx,
+ mih = ServerMigrationHelper(None, repo=repo, cnx=cnx, verbosity=0,
# hack so it don't try to load fs schema
schema=1)
else:
@@ -927,7 +943,7 @@
else:
mih.interactive_shell()
finally:
- if not self.config.pyro:
+ if not cnxtype: # shutdown in-memory repo
mih.shutdown()
else:
cnx.close()
--- a/cwvreg.py Wed Feb 22 11:57:42 2012 +0100
+++ b/cwvreg.py Tue Oct 23 15:00:53 2012 +0200
@@ -256,6 +256,12 @@
key=lambda x: x.cw_propval('order'))
+def related_appobject(obj, appobjectattr='__appobject__'):
+ """ adapts any object to a potential appobject bound to it
+ through the __appobject__ attribute
+ """
+ return getattr(obj, appobjectattr, obj)
+
class ETypeRegistry(CWRegistry):
@@ -272,6 +278,7 @@
self.clear_caches()
def register(self, obj, **kwargs):
+ obj = related_appobject(obj)
oid = kwargs.get('oid') or obj.__regid__
if oid != 'Any' and not oid in self.schema:
self.error('don\'t register %s, %s type not defined in the '
@@ -537,6 +544,20 @@
def itervalues(self):
return (value for key, value in self.items())
+ def load_module(self, module):
+ """ variation from the base implementation:
+ apply related_appobject to the automatically registered objects
+ """
+ self.info('loading %s from %s', module.__name__, module.__file__)
+ if hasattr(module, 'registration_callback'):
+ module.registration_callback(self)
+ return
+ for objname, obj in vars(module).iteritems():
+ if objname.startswith('_'):
+ continue
+ self._load_ancestors_then_object(module.__name__,
+ related_appobject(obj))
+
def reset(self):
CW_EVENT_MANAGER.emit('before-registry-reset', self)
super(CWRegistryStore, self).reset()
@@ -552,11 +573,22 @@
self.register_property(key, **propdef)
CW_EVENT_MANAGER.emit('after-registry-reset', self)
+ def register_all(self, objects, modname, butclasses=()):
+ butclasses = set(related_appobject(obj)
+ for obj in butclasses)
+ objects = [related_appobject(obj) for obj in objects]
+ super(CWRegistryStore, self).register_all(objects, modname, butclasses)
+
+ def register_and_replace(self, obj, replaced):
+ obj = related_appobject(obj)
+ replaced = related_appobject(replaced)
+ super(CWRegistryStore, self).register_and_replace(obj, replaced)
+
def set_schema(self, schema):
"""set instance'schema and load application objects"""
self._set_schema(schema)
# now we can load application's web objects
- self.reload(self.config.vregistry_path(), force_reload=False)
+ self.reload(self.config.appobjects_path(), force_reload=False)
# map lowered entity type names to their actual name
self.case_insensitive_etypes = {}
for eschema in self.schema.entities():
@@ -566,7 +598,7 @@
clear_cache(eschema, 'meta_attributes')
def reload_if_needed(self):
- path = self.config.vregistry_path()
+ path = self.config.appobjects_path()
if self.is_reload_needed(path):
self.reload(path)
@@ -582,7 +614,7 @@
cfg = self.config
for cube in cfg.expand_cubes(cubes, with_recommends=True):
if not cube in cubes:
- cpath = cfg.build_vregistry_cube_path([cfg.cube_dir(cube)])
+ cpath = cfg.build_appobjects_cube_path([cfg.cube_dir(cube)])
cleanup_sys_modules(cpath)
self.register_objects(path)
CW_EVENT_MANAGER.emit('after-registry-reload')
@@ -624,6 +656,7 @@
If `clear` is true, all objects with the same identifier will be
previously unregistered.
"""
+ obj = related_appobject(obj)
super(CWRegistryStore, self).register(obj, *args, **kwargs)
# XXX bw compat
ifaces = use_interfaces(obj)
--- a/dataimport.py Wed Feb 22 11:57:42 2012 +0100
+++ b/dataimport.py Tue Oct 23 15:00:53 2012 +0200
@@ -182,7 +182,10 @@
assert isinstance(row, dict)
assert isinstance(map, list)
for src, dest, funcs in map:
- res[dest] = row[src]
+ try:
+ res[dest] = row[src]
+ except KeyError:
+ continue
try:
for func in funcs:
res[dest] = func(res[dest])
@@ -446,9 +449,12 @@
if session is None:
sys.exit('please provide a session of run this script with cubicweb-ctl shell and pass cnx as session')
if not hasattr(session, 'set_cnxset'):
- # connection
- cnx = session
- session = session.request()
+ if hasattr(session, 'request'):
+ # connection object
+ cnx = session
+ session = session.request()
+ else: # object is already a request
+ cnx = session.cnx
session.set_cnxset = lambda : None
commit = commit or cnx.commit
else:
--- a/dbapi.py Wed Feb 22 11:57:42 2012 +0100
+++ b/dbapi.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -80,9 +80,8 @@
class ConnectionProperties(object):
- def __init__(self, cnxtype=None, lang=None, close=True, log=False):
+ def __init__(self, cnxtype=None, close=True, log=False):
self.cnxtype = cnxtype or 'pyro'
- self.lang = lang
self.log_queries = log
self.close_on_del = close
@@ -93,14 +92,18 @@
Only 'in-memory' and 'pyro' are supported for now. Either vreg or config
argument should be given
"""
- assert method in ('pyro', 'inmemory')
+ assert method in ('pyro', 'inmemory', 'zmq')
assert vreg or config
if vreg and not config:
config = vreg.config
if method == 'inmemory':
# get local access to the repository
from cubicweb.server.repository import Repository
- return Repository(config, vreg=vreg)
+ from cubicweb.server.utils import TasksManager
+ return Repository(config, TasksManager(), vreg=vreg)
+ elif method == 'zmq':
+ from cubicweb.zmqclient import ZMQRepositoryClient
+ return ZMQRepositoryClient(database)
else: # method == 'pyro'
# resolve the Pyro object
from logilab.common.pyro_ext import ns_get_proxy, get_proxy
@@ -145,8 +148,8 @@
the user login to use to authenticate.
:host:
- the pyro nameserver host. Will be detected using broadcast query if
- unspecified.
+ - pyro: nameserver host. Will be detected using broadcast query if unspecified
+ - zmq: repository host socket address
:group:
the instance's pyro nameserver group. You don't have to specify it unless
@@ -183,6 +186,8 @@
config.global_set_option('pyro-ns-host', host)
if group:
config.global_set_option('pyro-ns-group', group)
+ elif method == 'zmq':
+ config = cwconfig.CubicWebNoAppConfiguration()
else:
assert database
config = cwconfig.instance_configuration(database)
@@ -277,16 +282,18 @@
return '<DBAPISession %r>' % self.sessionid
class DBAPIRequest(RequestSessionBase):
+ #: Request language identifier eg: 'en'
+ lang = None
def __init__(self, vreg, session=None):
super(DBAPIRequest, self).__init__(vreg)
+ #: 'language' => translation_function() mapping
try:
# no vreg or config which doesn't handle translations
self.translations = vreg.config.translations
except AttributeError:
self.translations = {}
- self.set_default_language(vreg)
- # cache entities built during the request
+ #: cache entities built during the request
self._eid_cache = {}
if session is not None:
self.set_session(session)
@@ -295,6 +302,7 @@
# established
self.session = None
self.cnx = self.user = _NeedAuthAccessMock()
+ self.set_default_language(vreg)
def from_controller(self):
return 'view'
@@ -308,7 +316,7 @@
self.cnx = session.cnx
self.execute = session.cnx.cursor(self).execute
if user is None:
- user = self.cnx.user(self, {'lang': self.lang})
+ user = self.cnx.user(self)
if user is not None:
self.user = user
self.set_entity_cache(user)
@@ -321,19 +329,20 @@
def set_default_language(self, vreg):
try:
- self.lang = vreg.property_value('ui.language')
+ lang = vreg.property_value('ui.language')
except Exception: # property may not be registered
- self.lang = 'en'
- # use req.__ to translate a message without registering it to the catalog
+ lang = 'en'
try:
- gettext, pgettext = self.translations[self.lang]
- self._ = self.__ = gettext
- self.pgettext = pgettext
+ self.set_language(lang)
except KeyError:
# this occurs usually during test execution
self._ = self.__ = unicode
self.pgettext = lambda x, y: unicode(y)
- self.debug('request default language: %s', self.lang)
+
+ # server-side service call #################################################
+
+ def call_service(self, regid, async=False, **kwargs):
+ return self.cnx.call_service(regid, async, **kwargs)
# entities cache management ###############################################
@@ -556,6 +565,12 @@
except Exception:
pass
+ # server-side service call #################################################
+
+ @check_not_closed
+ def call_service(self, regid, async=False, **kwargs):
+ return self._repo.call_service(self.sessionid, regid, async, **kwargs)
+
# connection initialization methods ########################################
def load_appobjects(self, cubes=_MARKER, subpath=None, expand=True):
@@ -577,10 +592,15 @@
esubpath = list(subpath)
esubpath.remove('views')
esubpath.append(join('web', 'views'))
+ # first load available configs, necessary for proper persistent
+ # properties initialization
+ config.load_available_configs()
+ # then init cubes
config.init_cubes(cubes)
- vpath = config.build_vregistry_path(reversed(config.cubes_path()),
- evobjpath=esubpath,
- tvobjpath=subpath)
+ # then load appobjects into the registry
+ vpath = config.build_appobjects_path(reversed(config.cubes_path()),
+ evobjpath=esubpath,
+ tvobjpath=subpath)
self.vreg.register_objects(vpath)
def use_web_compatible_requests(self, baseurl, sitetitle=None):
@@ -650,11 +670,6 @@
# session data methods #####################################################
@check_not_closed
- def set_session_props(self, **props):
- """raise `BadConnectionId` if the connection is no more valid"""
- self._repo.set_session_props(self.sessionid, props)
-
- @check_not_closed
def get_shared_data(self, key, default=None, pop=False, txdata=False):
"""return value associated to key in the session's data dictionary or
session's transaction's data if `txdata` is true.
--- a/debian/changelog Wed Feb 22 11:57:42 2012 +0100
+++ b/debian/changelog Tue Oct 23 15:00:53 2012 +0200
@@ -1,3 +1,69 @@
+cubicweb (3.15.4-1) unstable; urgency=low
+
+ * New upstream release
+
+ -- Julien Cristau <jcristau@debian.org> Fri, 31 Aug 2012 16:43:11 +0200
+
+cubicweb (3.15.3-1) unstable; urgency=low
+
+ * New upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@logilab.fr> Tue, 21 Aug 2012 14:19:31 +0200
+
+cubicweb (3.15.2-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr> Fri, 20 Jul 2012 15:17:17 +0200
+
+cubicweb (3.15.1-1) quantal; urgency=low
+
+ * new upstream release
+
+ -- David Douard <david.douard@logilab.fr> Mon, 11 Jun 2012 09:45:24 +0200
+
+cubicweb (3.15.0-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr> Thu, 12 Apr 2012 13:52:05 +0200
+
+cubicweb (3.14.9-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Pierre-Yves David <pierre-yves.david@logilab.fr> Tue, 31 Jul 2012 16:16:28 +0200
+
+cubicweb (3.14.8-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr> Wed, 23 May 2012 11:42:54 +0200
+
+cubicweb (3.14.7-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- David Douard <david.douard@logilab.fr> Wed, 11 Apr 2012 09:28:46 +0200
+
+cubicweb (3.14.6-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr> Tue, 13 Mar 2012 14:21:04 +0100
+
+cubicweb (3.14.5-1) unstable; urgency=low
+
+ * New upstream release
+
+ -- David Douard <david.douard@logilab.fr> Thu, 01 Mar 2012 15:29:29 +0100
+
+cubicweb (3.14.4-2) unstable; urgency=low
+
+ * add missing build-deps to generate the documentation
+
+ -- David Douard <david.douard@logilab.fr> Wed, 29 Feb 2012 17:00:52 +0100
+
cubicweb (3.14.4-1) unstable; urgency=low
* New upstream release
--- a/debian/control Wed Feb 22 11:57:42 2012 +0100
+++ b/debian/control Tue Oct 23 15:00:53 2012 +0200
@@ -7,18 +7,25 @@
Adrien Di Mascio <Adrien.DiMascio@logilab.fr>,
Aurélien Campéas <aurelien.campeas@logilab.fr>,
Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 7), python (>= 2.5), python-central (>= 0.5), python-sphinx
-# for the documentation:
-# python-sphinx, python-logilab-common, python-unittest2, logilab-doctools, logilab-xml
+Build-Depends:
+ debhelper (>= 7),
+ python (>= 2.5),
+ python-central (>= 0.5),
+ python-sphinx,
+ python-logilab-common,
+ python-unittest2,
+ python-logilab-mtconverter,
+ python-rql,
+ python-yams,
+ python-lxml,
Standards-Version: 3.9.1
Homepage: http://www.cubicweb.org
-XS-Python-Version: >= 2.5, << 3.0
+XS-Python-Version: >= 2.5
Package: cubicweb
Architecture: all
XB-Python-Version: ${python:Versions}
Depends: ${misc:Depends}, ${python:Depends}, cubicweb-server (= ${source:Version}), cubicweb-twisted (= ${source:Version})
-XB-Recommends: (postgresql, postgresql-plpython) | mysql | sqlite3
Recommends: postgresql | mysql | sqlite3
Description: the complete CubicWeb framework
CubicWeb is a semantic web application framework.
@@ -35,7 +42,7 @@
Conflicts: cubicweb-multisources
Replaces: cubicweb-multisources
Provides: cubicweb-multisources
-Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2, python-passlib
Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version})
Suggests: python-zmq
Description: server part of the CubicWeb framework
@@ -85,8 +92,8 @@
Package: cubicweb-web
Architecture: all
XB-Python-Version: ${python:Versions}
-Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 1.3)
-Recommends: python-docutils, python-vobject, fckeditor, python-fyzz, python-imaging, python-rdflib
+Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), python-simplejson (>= 2.0.9)
+Recommends: python-docutils (>= 0.6), python-vobject, fckeditor, python-fyzz, python-imaging, python-rdflib
Description: web interface library for the CubicWeb framework
CubicWeb is a semantic web application framework.
.
@@ -100,7 +107,7 @@
Package: cubicweb-common
Architecture: all
XB-Python-Version: ${python:Versions}
-Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.58.0), python-yams (>= 0.34.0), python-rql (>= 0.28.0), python-lxml
+Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.58.0), python-yams (>= 0.36.0), python-rql (>= 0.31.2), python-lxml
Recommends: python-simpletal (>= 4.0), python-crypto
Conflicts: cubicweb-core
Replaces: cubicweb-core
--- a/debian/rules Wed Feb 22 11:57:42 2012 +0100
+++ b/debian/rules Tue Oct 23 15:00:53 2012 +0200
@@ -11,10 +11,14 @@
build-stamp:
dh_testdir
NO_SETUPTOOLS=1 python setup.py build
+ # cubicweb.foo needs to be importable by sphinx, so create a cubicweb symlink to the source dir
+ mkdir -p debian/pythonpath
+ ln -sf $(CURDIR) debian/pythonpath/cubicweb
# documentation build is now made optional since it can break for old
# distributions and we don't want to block a new release of Cubicweb
# because of documentation issues.
- -PYTHONPATH=$(CURDIR)/.. $(MAKE) -C doc/book/en all
+ -PYTHONPATH=$${PYTHONPATH:+$${PYTHONPATH}:}$(CURDIR)/debian/pythonpath $(MAKE) -C doc/book/en all
+ rm -rf debian/pythonpath
touch build-stamp
clean:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/watch Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,2 @@
+version=3
+http://download.logilab.org/pub/cubicweb cubicweb-(.*)\.tar\.gz
--- a/devtools/__init__.py Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/__init__.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -168,7 +168,7 @@
def load_configuration(self):
super(TestServerConfiguration, self).load_configuration()
# no undo support in tests
- self.global_set_option('undo-support', '')
+ self.global_set_option('undo-enabled', 'n')
def main_config_file(self):
"""return instance's control configuration file"""
@@ -355,7 +355,7 @@
def _restore_database(self, backup_coordinates, config):
"""Actual restore of the current database.
- Use the value tostored in db_cache as input """
+ Use the value stored in db_cache as input """
raise NotImplementedError()
def get_repo(self, startup=False):
@@ -466,7 +466,6 @@
``pre_setup_func`` to setup the database.
This function backup any database it build"""
-
if self.has_cache(test_db_id):
return #test_db_id, 'already in cache'
if test_db_id is DEFAULT_EMPTY_DB_ID:
@@ -723,7 +722,7 @@
dbfile = self.absolute_dbfile()
self._cleanup_database(dbfile)
shutil.copy(backup_coordinates, dbfile)
- repo = self.get_repo()
+ self.get_repo()
def init_test_database(self):
"""initialize a fresh sqlite databse used for testing purpose"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/data/xvfb-run.sh Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,190 @@
+#!/bin/sh
+
+# This script starts an instance of Xvfb, the "fake" X server, runs a command
+# with that server available, and kills the X server when done. The return
+# value of the command becomes the return value of this script.
+#
+# If anyone is using this to build a Debian package, make sure the package
+# Build-Depends on xvfb and xauth.
+
+set -e
+
+PROGNAME=xvfb-run
+SERVERNUM=99
+AUTHFILE=
+ERRORFILE=/dev/null
+XVFBARGS="-screen 0 640x480x8"
+LISTENTCP="-nolisten tcp"
+XAUTHPROTO=.
+
+# Query the terminal to establish a default number of columns to use for
+# displaying messages to the user. This is used only as a fallback in the event
+# the COLUMNS variable is not set. ($COLUMNS can react to SIGWINCH while the
+# script is running, and this cannot, only being calculated once.)
+DEFCOLUMNS=$(stty size 2>/dev/null | awk '{print $2}') || true
+if ! expr "$DEFCOLUMNS" : "[[:digit:]]\+$" >/dev/null 2>&1; then
+ DEFCOLUMNS=80
+fi
+
+# Display a message, wrapping lines at the terminal width.
+message () {
+ echo "$PROGNAME: $*" | fmt -t -w ${COLUMNS:-$DEFCOLUMNS}
+}
+
+# Display an error message.
+error () {
+ message "error: $*" >&2
+}
+
+# Display a usage message.
+usage () {
+ if [ -n "$*" ]; then
+ message "usage error: $*"
+ fi
+ cat <<EOF
+Usage: $PROGNAME [OPTION ...] COMMAND
+Run COMMAND (usually an X client) in a virtual X server environment.
+Options:
+-a --auto-servernum try to get a free server number, starting at
+ --server-num
+-e FILE --error-file=FILE file used to store xauth errors and Xvfb
+ output (default: $ERRORFILE)
+-f FILE --auth-file=FILE file used to store auth cookie
+ (default: ./.Xauthority)
+-h --help display this usage message and exit
+-n NUM --server-num=NUM server number to use (default: $SERVERNUM)
+-l --listen-tcp enable TCP port listening in the X server
+-p PROTO --xauth-protocol=PROTO X authority protocol name to use
+ (default: xauth command's default)
+-s ARGS --server-args=ARGS arguments (other than server number and
+ "-nolisten tcp") to pass to the Xvfb server
+ (default: "$XVFBARGS")
+EOF
+}
+
+# Find a free server number by looking at .X*-lock files in /tmp.
+find_free_servernum() {
+ # Sadly, the "local" keyword is not POSIX. Leave the next line commented in
+ # the hope Debian Policy eventually changes to allow it in /bin/sh scripts
+ # anyway.
+ #local i
+
+ i=$SERVERNUM
+ while [ -f /tmp/.X$i-lock ]; do
+ i=$(($i + 1))
+ done
+ echo $i
+}
+
+# Clean up files
+clean_up() {
+ if [ -e "$AUTHFILE" ]; then
+ XAUTHORITY=$AUTHFILE xauth remove ":$SERVERNUM" >>"$ERRORFILE" 2>&1
+ fi
+ if [ -n "$XVFB_RUN_TMPDIR" ]; then
+ if ! rm -r "$XVFB_RUN_TMPDIR"; then
+ error "problem while cleaning up temporary directory"
+ exit 5
+ fi
+ fi
+ if [ -n "$XVFBPID" ]; then
+ kill "$XVFBPID"
+ fi
+}
+
+# Parse the command line.
+ARGS=$(getopt --options +ae:f:hn:lp:s:w: \
+ --long auto-servernum,error-file:,auth-file:,help,server-num:,listen-tcp,xauth-protocol:,server-args:,wait: \
+ --name "$PROGNAME" -- "$@")
+GETOPT_STATUS=$?
+
+if [ $GETOPT_STATUS -ne 0 ]; then
+ error "internal error; getopt exited with status $GETOPT_STATUS"
+ exit 6
+fi
+
+eval set -- "$ARGS"
+
+while :; do
+ case "$1" in
+ -a|--auto-servernum) SERVERNUM=$(find_free_servernum); AUTONUM="yes" ;;
+ -e|--error-file) ERRORFILE="$2"; shift ;;
+ -f|--auth-file) AUTHFILE="$2"; shift ;;
+ -h|--help) SHOWHELP="yes" ;;
+ -n|--server-num) SERVERNUM="$2"; shift ;;
+ -l|--listen-tcp) LISTENTCP="" ;;
+ -p|--xauth-protocol) XAUTHPROTO="$2"; shift ;;
+ -s|--server-args) XVFBARGS="$2"; shift ;;
+ -w|--wait) shift ;;
+ --) shift; break ;;
+ *) error "internal error; getopt permitted \"$1\" unexpectedly"
+ exit 6
+ ;;
+ esac
+ shift
+done
+
+if [ "$SHOWHELP" ]; then
+ usage
+ exit 0
+fi
+
+if [ -z "$*" ]; then
+ usage "need a command to run" >&2
+ exit 2
+fi
+
+if ! which xauth >/dev/null; then
+ error "xauth command not found"
+ exit 3
+fi
+
+# tidy up after ourselves
+trap clean_up EXIT
+
+# If the user did not specify an X authorization file to use, set up a temporary
+# directory to house one.
+if [ -z "$AUTHFILE" ]; then
+ XVFB_RUN_TMPDIR="$(mktemp -d -t $PROGNAME.XXXXXX)"
+ # Create empty file to avoid xauth warning
+ AUTHFILE=$(tempfile -n "$XVFB_RUN_TMPDIR/Xauthority")
+fi
+
+# Start Xvfb.
+MCOOKIE=$(mcookie)
+tries=10
+while [ $tries -gt 0 ]; do
+ tries=$(( $tries - 1 ))
+ XAUTHORITY=$AUTHFILE xauth source - << EOF >>"$ERRORFILE" 2>&1
+add :$SERVERNUM $XAUTHPROTO $MCOOKIE
+EOF
+ # handle SIGUSR1 so Xvfb knows to send a signal when it's ready to accept
+ # connections
+ trap : USR1
+ (trap '' USR1; XAUTHORITY=$AUTHFILE exec Xvfb ":$SERVERNUM" $XVFBARGS $LISTENTCP >>"$ERRORFILE" 2>&1) &
+ XVFBPID=$!
+
+ wait || :
+ if kill -0 $XVFBPID 2>/dev/null; then
+ break
+ elif [ -n "$AUTONUM" ]; then
+ # The display is in use so try another one (if '-a' was specified).
+ SERVERNUM=$((SERVERNUM + 1))
+ SERVERNUM=$(find_free_servernum)
+ continue
+ fi
+ error "Xvfb failed to start" >&2
+ XVFBPID=
+ exit 1
+done
+
+# Start the command and save its exit status.
+set +e
+DISPLAY=:$SERVERNUM XAUTHORITY=$AUTHFILE "$@" 2>&1
+RETVAL=$?
+set -e
+
+# Return the executed command's exit status.
+exit $RETVAL
+
+# vim:set ai et sts=4 sw=4 tw=80:
--- a/devtools/devctl.py Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/devctl.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -91,7 +91,7 @@
if mod.__file__ is None:
# odd/rare but real
continue
- for path in config.vregistry_path():
+ for path in config.appobjects_path():
if mod.__file__.startswith(path):
del sys.modules[name]
break
@@ -303,7 +303,7 @@
from logilab.common.shellutils import globfind, find, rm
from logilab.common.modutils import get_module_files
from cubicweb.i18n import extract_from_tal, execute
- tempdir = tempfile.mkdtemp()
+ tempdir = tempfile.mkdtemp(prefix='cw-')
cwi18ndir = WebConfiguration.i18n_lib_dir()
print '-> extract schema messages.'
schemapot = osp.join(tempdir, 'schema.pot')
@@ -726,7 +726,7 @@
min_args = max_args = 1
options = [
('output-file',
- {'type':'file', 'default': None,
+ {'type':'string', 'default': None,
'metavar': '<file>', 'short':'o', 'help':'output image file',
'input':False,
}),
--- a/devtools/fake.py Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/fake.py Tue Oct 23 15:00:53 2012 +0200
@@ -33,6 +33,7 @@
class FakeConfig(dict, BaseApptestConfiguration):
translations = {}
uiprops = {}
+ https_uiprops = {}
apphome = None
debugmode = False
def __init__(self, appid='data', apphome=None, cubes=()):
@@ -44,6 +45,7 @@
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
@@ -59,10 +61,10 @@
if not (args or 'vreg' in kwargs):
kwargs['vreg'] = CWRegistryStore(FakeConfig(), initlog=False)
kwargs['https'] = False
+ self._http_method = kwargs.pop('method', 'GET')
self._url = kwargs.pop('url', None) or 'view?rql=Blop&vid=blop'
super(FakeRequest, self).__init__(*args, **kwargs)
self._session_data = {}
- self._headers_in = Headers()
def set_cookie(self, name, value, maxage=300, expires=None, secure=False):
super(FakeRequest, self).set_cookie(name, value, maxage, expires, secure)
@@ -74,8 +76,8 @@
"""returns an ordered list of preferred languages"""
return ('en',)
- def header_if_modified_since(self):
- return None
+ def http_method(self):
+ return self._http_method
def relative_path(self, includeparams=True):
"""return the normalized path of the request (ie at least relative
@@ -90,35 +92,23 @@
return url
return url.split('?', 1)[0]
- def get_header(self, header, default=None, raw=True):
- """return the value associated with the given input header, raise
- KeyError if the header is not set
- """
- if raw:
- return self._headers_in.getRawHeaders(header, [default])[0]
- return self._headers_in.getHeader(header, default)
-
- ## extend request API to control headers in / out values
def set_request_header(self, header, value, raw=False):
- """set an input HTTP header"""
+ """set an incoming HTTP header (For test purpose only)"""
if isinstance(value, basestring):
value = [value]
- if raw:
+ if raw: #
+ # adding encoded header is important, else page content
+ # will be reconverted back to unicode and apart unefficiency, this
+ # may cause decoding problem (e.g. when downloading a file)
self._headers_in.setRawHeaders(header, value)
- else:
- self._headers_in.setHeader(header, value)
+ else: #
+ self._headers_in.setHeader(header, value) #
def get_response_header(self, header, default=None, raw=False):
- """return the value associated with the given input header,
- raise KeyError if the header is not set
- """
- if raw:
- return self.headers_out.getRawHeaders(header, default)[0]
- else:
- return self.headers_out.getHeader(header, default)
-
- def validate_cache(self):
- pass
+ """return output header (For test purpose only"""
+ if raw: #
+ return self.headers_out.getRawHeaders(header, [default])[0]
+ return self.headers_out.getHeader(header, default)
def build_url_params(self, **kwargs):
# overriden to get predictable resultts
@@ -165,6 +155,12 @@
def set_entity_cache(self, entity):
pass
+ def security_enabled(self, read=False, write=False):
+ class FakeCM(object):
+ def __enter__(self): pass
+ def __exit__(self, exctype, exc, traceback): pass
+ return FakeCM()
+
# for use with enabled_security context manager
read_security = write_security = True
def init_security(self, *args):
--- a/devtools/qunit.py Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/qunit.py Tue Oct 23 15:00:53 2012 +0200
@@ -64,7 +64,7 @@
def __init__(self, url=None):
self._process = None
- self._tmp_dir = mkdtemp()
+ self._tmp_dir = mkdtemp(prefix='cwtest-ffxprof-')
self._profile_data = {'uid': uuid4()}
self._profile_name = self.profile_name_mask % self._profile_data
fnull = open(os.devnull, 'w')
@@ -72,7 +72,7 @@
stderr = TemporaryFile()
self.firefox_cmd = ['firefox', '-no-remote']
if os.name == 'posix':
- self.firefox_cmd = ['xvfb-run', '-a'] + self.firefox_cmd
+ self.firefox_cmd = [osp.join(osp.dirname(__file__), 'data', 'xvfb-run.sh'), '-a'] + self.firefox_cmd
try:
home = osp.expanduser('~')
user = getlogin()
--- a/devtools/repotest.py Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/repotest.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/devtools/test/unittest_testlib.py Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/test/unittest_testlib.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""unittests for cw.devtools.testlib module"""
+from __future__ import with_statement
from cStringIO import StringIO
@@ -155,5 +156,20 @@
self.assertEqual(self.page_info.has_link_regexp('L[ai]gilab'), False)
+class CWUtilitiesTC(CubicWebTC):
+ def test_temporary_permissions_eschema(self):
+ eschema = self.schema['CWUser']
+ with self.temporary_permissions(CWUser={'read': ()}):
+ self.assertEqual(eschema.permissions['read'], ())
+ self.assertTrue(eschema.permissions['add'])
+ self.assertTrue(eschema.permissions['read'], ())
+
+ def test_temporary_permissions_rdef(self):
+ rdef = self.schema['CWUser'].rdef('in_group')
+ with self.temporary_permissions((rdef, {'read': ()})):
+ self.assertEqual(rdef.permissions['read'], ())
+ self.assertTrue(rdef.permissions['add'])
+ self.assertTrue(rdef.permissions['read'], ())
+
if __name__ == '__main__':
unittest_main()
--- a/devtools/testlib.py Wed Feb 22 11:57:42 2012 +0100
+++ b/devtools/testlib.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -31,6 +31,7 @@
from contextlib import contextmanager
from warnings import warn
from types import NoneType
+from itertools import chain
import yams.schema
@@ -46,7 +47,7 @@
from cubicweb import cwconfig, dbapi, devtools, web, server
from cubicweb.sobjects import notification
from cubicweb.web import Redirect, application
-from cubicweb.server.session import Session, security_enabled
+from cubicweb.server.session import Session
from cubicweb.server.hook import SendMailOp
from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
from cubicweb.devtools import BASE_URL, fake, htmlparser, DEFAULT_EMPTY_DB_ID
@@ -444,7 +445,7 @@
finally:
self.session.set_cnxset() # ensure cnxset still set after commit
- # # server side db api #######################################################
+ # server side db api #######################################################
def sexecute(self, rql, args=None, eid_key=None):
if eid_key is not None:
@@ -466,6 +467,51 @@
for obj in appobjects:
self.vreg.unregister(obj)
+ @contextmanager
+ def temporary_permissions(self, *perm_overrides, **perm_kwoverrides):
+ """Set custom schema permissions within context.
+
+ There are two ways to call this method, which may be used together :
+
+ * using positional argument(s):
+
+ .. sourcecode:: python
+
+ rdef = self.schema['CWUser'].rdef('login')
+ with self.temporary_permissions((rdef, {'read': ()})):
+ ...
+
+
+ * using named argument(s):
+
+ .. sourcecode:: python
+
+ rdef = self.schema['CWUser'].rdef('login')
+ with self.temporary_permissions(CWUser={'read': ()}):
+ ...
+
+ Usually the former will be prefered to override permissions on a
+ relation definition, while the latter is well suited for entity types.
+
+ The allowed keys in the permission dictionary depends on the schema type
+ (entity type / relation definition). Resulting permissions will be
+ similar to `orig_permissions.update(partial_perms)`.
+ """
+ torestore = []
+ for erschema, etypeperms in chain(perm_overrides, perm_kwoverrides.iteritems()):
+ if isinstance(erschema, basestring):
+ erschema = self.schema[erschema]
+ for action, actionperms in etypeperms.iteritems():
+ origperms = erschema.permissions[action]
+ erschema.set_action_permissions(action, actionperms)
+ torestore.append([erschema, action, origperms])
+ yield
+ for erschema, action, permissions in torestore:
+ if action is None:
+ erschema.permissions = permissions
+ else:
+ erschema.set_action_permissions(action, permissions)
+
def assertModificationDateGreater(self, entity, olddate):
entity.cw_attr_cache.pop('modification_date', None)
self.assertTrue(entity.modification_date > olddate)
@@ -592,9 +638,9 @@
return publisher
requestcls = fake.FakeRequest
- def request(self, rollbackfirst=False, url=None, **kwargs):
+ def request(self, rollbackfirst=False, url=None, headers={}, **kwargs):
"""return a web ui request"""
- req = self.requestcls(self.vreg, url=url, form=kwargs)
+ req = self.requestcls(self.vreg, url=url, headers=headers, form=kwargs)
if rollbackfirst:
self.websession.cnx.rollback()
req.set_session(self.websession)
@@ -608,8 +654,13 @@
ctrl = self.vreg['controllers'].select('ajax', req)
return ctrl.publish(), req
- def app_publish(self, req, path='view'):
- return self.app.publish(path, req)
+ def app_handle_request(self, req, path='view'):
+ return self.app.core_handle(req, path)
+
+ @deprecated("[3.15] app_handle_request is the new and better way"
+ " (beware of small semantic changes)")
+ def app_publish(self, *args, **kwargs):
+ return self.app_handle_request(*args, **kwargs)
def ctrl_publish(self, req, ctrl='edit'):
"""call the publish method of the edit controller"""
@@ -646,6 +697,20 @@
ctrlid, rset = self.app.url_resolver.process(req, req.relative_path(False))
return self.ctrl_publish(req, ctrlid)
+ @staticmethod
+ def _parse_location(req, location):
+ try:
+ path, params = location.split('?', 1)
+ except ValueError:
+ path = location
+ params = {}
+ else:
+ cleanup = lambda p: (p[0], unquote(p[1]))
+ params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
+ if path.startswith(req.base_url()): # may be relative
+ path = path[len(req.base_url()):]
+ return path, params
+
def expect_redirect(self, callback, req):
"""call the given callback with req as argument, expecting to get a
Redirect exception
@@ -653,25 +718,24 @@
try:
callback(req)
except Redirect, ex:
- try:
- path, params = ex.location.split('?', 1)
- except ValueError:
- path = ex.location
- params = {}
- else:
- cleanup = lambda p: (p[0], unquote(p[1]))
- params = dict(cleanup(p.split('=', 1)) for p in params.split('&') if p)
- if path.startswith(req.base_url()): # may be relative
- path = path[len(req.base_url()):]
- return path, params
+ return self._parse_location(req, ex.location)
else:
self.fail('expected a Redirect exception')
- def expect_redirect_publish(self, req, path='edit'):
+ def expect_redirect_handle_request(self, req, path='edit'):
"""call the publish method of the application publisher, expecting to
get a Redirect exception
"""
- return self.expect_redirect(lambda x: self.app_publish(x, path), req)
+ result = self.app_handle_request(req, path)
+ self.assertTrue(300 <= req.status_out <400, req.status_out)
+ location = req.get_response_header('location')
+ return self._parse_location(req, location)
+
+ @deprecated("[3.15] expect_redirect_handle_request is the new and better way"
+ " (beware of small semantic changes)")
+ def expect_redirect_publish(self, *args, **kwargs):
+ return self.expect_redirect_handle_request(*args, **kwargs)
+
def set_auth_mode(self, authmode, anonuser=None):
self.set_option('auth-mode', authmode)
@@ -697,13 +761,11 @@
def assertAuthSuccess(self, req, origsession, nbsessions=1):
sh = self.app.session_handler
- path, params = self.expect_redirect(lambda x: self.app.connect(x), req)
+ self.app.connect(req)
session = req.session
self.assertEqual(len(self.open_sessions), nbsessions, self.open_sessions)
self.assertEqual(session.login, origsession.login)
self.assertEqual(session.anonymous_session, False)
- self.assertEqual(path, 'view')
- self.assertMessageEqual(req, params, 'welcome %s !' % req.user.login)
def assertAuthFailure(self, req, nbsessions=0):
self.app.connect(req)
@@ -755,9 +817,8 @@
"""
req = req or rset and rset.req or self.request()
req.form['vid'] = vid
- kwargs['rset'] = rset
viewsreg = self.vreg['views']
- view = viewsreg.select(vid, req, **kwargs)
+ view = viewsreg.select(vid, req, rset=rset, **kwargs)
# set explicit test description
if rset is not None:
self.set_description("testing vid=%s defined in %s with (%s)" % (
@@ -769,10 +830,8 @@
viewfunc = view.render
else:
kwargs['view'] = view
- templateview = viewsreg.select(template, req, **kwargs)
viewfunc = lambda **k: viewsreg.main_template(req, template,
- **kwargs)
- kwargs.pop('rset')
+ rset=rset, **kwargs)
return self._test_view(viewfunc, view, template, kwargs)
@@ -991,7 +1050,7 @@
"""this method populates the database with `how_many` entities
of each possible type. It also inserts random relations between them
"""
- with security_enabled(self.session, read=False, write=False):
+ with self.session.security_enabled(read=False, write=False):
self._auto_populate(how_many)
def _auto_populate(self, how_many):
--- a/doc/3.15.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/3.15.rst Tue Oct 23 15:00:53 2012 +0200
@@ -1,6 +1,30 @@
-Whats new in CubicWeb 3.15
-==========================
+What's new in CubicWeb 3.15?
+============================
+
+New functionnalities
+--------------------
+
+* Add Zmq server, based on the cutting edge ZMQ (http://www.zeromq.org/) socket
+ library. This allows to access distant instance, in a similar way as Pyro.
+
+* Publish/subscribe mechanism using ZMQ for communication among cubicweb
+ instances. The new zmq-address-sub and zmq-address-pub configuration variables
+ define where this communication occurs. As of this release this mechanism is
+ used for entity cache invalidation.
+* Improved WSGI support. While there is still some caveats, most of the code
+ which as twisted only is now generic and allows related functionalities to work
+ with a WSGI front-end.
+
+* Full undo/transaction support : undo of modification has eventually been
+ implemented, and the configuration simplified (basically you activate it or not
+ on an instance basis).
+
+* Controlling HTTP status code used is not much more easier :
+
+ - `WebRequest` now has a `status_out` attribut to control the response status ;
+
+ - most web-side exceptions take an optional ``status`` argument.
API changes
-----------
@@ -25,38 +49,48 @@
* on the CubicWeb side, the `selectors` module has been renamed to
`predicates`.
- Debugging refactoring dropped the more need for the `lltrace` decorator.
-
- There should be full backward compat with proper deprecation warnings.
-
- Notice the `yes` predicate and `objectify_predicate` decorator, as well as the
+ Debugging refactoring dropped the more need for the `lltrace` decorator. There
+ should be full backward compat with proper deprecation warnings. Notice the
+ `yes` predicate and `objectify_predicate` decorator, as well as the
`traced_selection` function should now be imported from the
`logilab.common.registry` module.
+* All login forms are now submitted to <app_root>/login. Redirection to requested
+ page is now handled by the login controller (it was previously handle by the
+ session manager).
+
+* `Publisher.publish` has been renamed to `Publisher.handle_request`. This
+ method now contains generic version of logic previously handled by
+ Twisted. `Controller.publish` is **not** affected.
Unintrusive API changes
-----------------------
-* new 'ldapfeed' source type, designed to replace 'ldapuser' source with
+* New 'ldapfeed' source type, designed to replace 'ldapuser' source with
data-feed (i.e. copy based) source ideas.
+* New 'zmqrql' source type, similar to 'pyrorql' but using ømq instead of Pyro.
-RQL
----
+* A new registry called `services` has appeared, where you can register
+ server-side `cubicweb.server.Service` child classes. Their `call` method can be
+ invoked from a web-side AppObject instance using new `self._cw.call_service`
+ method or a server-side one using `self.session.call_service`. This is a new
+ way to call server-side methods, much cleaner than monkey patching the
+ Repository class, which becomes a deprecated way to perform similar tasks.
+* a new `ajax-func` registry now hosts all remote functions (i.e. functions
+ callable through the `asyncRemoteExec` JS api). A convenience `ajaxfunc`
+ decorator will let you expose your python function easily without all the
+ appobject standard boilerplate. Backward compatibility is preserved.
+
+* the 'json' controller is now deprecated in favor of the 'ajax' one.
+
+* `WebRequest.build_url` can now take a __secure__ argument. When True cubicweb
+ try to generate an https url.
User interface changes
----------------------
-
-
-Configuration
--------------
-
-Base schema changes
--------------------
-Email address 'read' permission is now more restrictive: only managers and
-users to which an address belong may see them. Application that wish other
-settings should set them explicitly.
-
+A new 'undohistory' view expose the undoable transactions and give access to undo
+some of them.
--- a/doc/book/en/admin/config.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/admin/config.rst Tue Oct 23 15:00:53 2012 +0200
@@ -70,53 +70,53 @@
install the `postgresql-client` package on the |cubicweb| host, and others on the
database host.
-.. Note::
+Database cluster
+++++++++++++++++
- If you already have an existing cluster and PostgreSQL server running, you do
- not need to execute the initilization step of your PostgreSQL database unless
- you want a specific cluster for |cubicweb| databases or if your existing
- cluster doesn't use the UTF8 encoding (see note below).
+If you already have an existing cluster and PostgreSQL server running, you do
+not need to execute the initilization step of your PostgreSQL database unless
+you want a specific cluster for |cubicweb| databases or if your existing
+cluster doesn't use the UTF8 encoding (see note below).
-* First, initialize a PostgreSQL cluster with the command ``initdb``::
+To initialize a PostgreSQL cluster, use the command ``initdb``::
$ initdb -E UTF8 -D /path/to/pgsql
- Notice the encoding specification. This is necessary since |cubicweb| usually
- want UTF8 encoded database. If you use a cluster with the wrong encoding, you'll
- get error like::
+Notice the encoding specification. This is necessary since |cubicweb| usually
+want UTF8 encoded database. If you use a cluster with the wrong encoding, you'll
+get error like::
- new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII)
- HINT: Use the same encoding as in the template database, or use template0 as template.
-
+ new encoding (UTF8) is incompatible with the encoding of the template database (SQL_ASCII)
+ HINT: Use the same encoding as in the template database, or use template0 as template.
- Once initialized, start the database server PostgreSQL with the command::
+Once initialized, start the database server PostgreSQL with the command::
- $ postgres -D /path/to/psql
+ $ postgres -D /path/to/psql
- If you cannot execute this command due to permission issues, please make sure
- that your username has write access on the database. ::
+If you cannot execute this command due to permission issues, please make sure
+that your username has write access on the database. ::
- $ chown username /path/to/pgsql
+ $ chown username /path/to/pgsql
-* The database authentication can be either set to `ident sameuser` or `md5`. If
- set to `md5`, make sure to use an existing user of your database. If set to
- `ident sameuser`, make sure that your client's operating system user name has a
- matching user in the database. If not, please do as follow to create a user::
+Database authentication
++++++++++++++++++++++++
- $ su
- $ su - postgres
- $ createuser -s -P username
+The database authentication is configured in `pg_hba.conf`. It can be either set
+to `ident sameuser` or `md5`. If set to `md5`, make sure to use an existing
+user of your database. If set to `ident sameuser`, make sure that your client's
+operating system user name has a matching user in the database. If not, please
+do as follow to create a user::
- The option `-P` (for password prompt), will encrypt the password with the
- method set in the configuration file :file:`pg_hba.conf`. If you do not use this
- option `-P`, then the default value will be null and you will need to set it
- with::
+ $ su
+ $ su - postgres
+ $ createuser -s -P username
- $ su postgres -c "echo ALTER USER username WITH PASSWORD 'userpasswd' | psql"
+The option `-P` (for password prompt), will encrypt the password with the
+method set in the configuration file :file:`pg_hba.conf`. If you do not use this
+option `-P`, then the default value will be null and you will need to set it
+with::
-.. Note::
- The authentication method can be configured in file:`pg_hba.conf`.
-
+ $ su postgres -c "echo ALTER USER username WITH PASSWORD 'userpasswd' | psql"
The above login/password will be requested when you will create an instance with
`cubicweb-ctl create` to initialize the database of your instance.
@@ -149,7 +149,6 @@
cat /usr/share/postgresql/8.X/contrib/tsearch2.sql | psql -U username template1
-
.. _MySqlConfiguration:
MySql
@@ -196,12 +195,12 @@
The ALTER DATABASE command above requires some permissions that your
user may not have. In that case you will have to ask your local DBA to
-run the query for you.
+run the query for you.
You can check that the setting is correct by running the following
query which must return '1'::
- SELECT is_read_committed_snapshot_on
+ SELECT is_read_committed_snapshot_on
FROM sys.databases WHERE name='<databasename>';
@@ -210,6 +209,7 @@
SQLite
~~~~~~
+
SQLite has the great advantage of requiring almost no configuration. Simply
use 'sqlite' as db-driver, and set path to the dabase as db-name. Don't specify
anything for db-user and db-password, they will be ignore anyway.
@@ -226,6 +226,7 @@
Pyro name server
~~~~~~~~~~~~~~~~
+
If you want to use Pyro to access your instance remotely, or to have multi-source
or distributed configuration, it is required to have a Pyro name server running
on your network. By default it is detected by a broadcast request, but you can
--- a/doc/book/en/admin/instance-config.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/admin/instance-config.rst Tue Oct 23 15:00:53 2012 +0200
@@ -17,6 +17,7 @@
each option name is prefixed with its own section and followed by its
default value if necessary, e.g. "`<section>.<option>` [value]."
+.. _`WebServerConfig`:
Configuring the Web server
--------------------------
--- a/doc/book/en/admin/ldap.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/admin/ldap.rst Tue Oct 23 15:00:53 2012 +0200
@@ -29,6 +29,15 @@
The base functionality for this is in
:file:`cubicweb/server/sources/ldapuser.py`.
+External dependencies
+---------------------
+
+You'll need the following packages to make CubicWeb interact with your LDAP /
+Active Directory server:
+
+* python-ldap
+* ldaputils if using `ldapfeed` source
+
Configurations options
----------------------
--- a/doc/book/en/admin/setup.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/admin/setup.rst Tue Oct 23 15:00:53 2012 +0200
@@ -63,9 +63,9 @@
deb http://download.logilab.org/production/ lucid/
- Note that for Ubuntu Maverick and newer, you shall use the `lucid`
- repository and install the ``libgecode19`` package from `lucid
- universe <http://packages.ubuntu.com/lucid/libgecode19>`_.
+Note that for Ubuntu Maverick and newer, you shall use the `lucid`
+repository and install the ``libgecode19`` package from `lucid
+universe <http://packages.ubuntu.com/lucid/libgecode19>`_.
The repositories are signed with the `Logilab's gnupg key`_. You can download
and register the key to avoid warnings::
--- a/doc/book/en/annexes/depends.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/annexes/depends.rst Tue Oct 23 15:00:53 2012 +0200
@@ -45,6 +45,9 @@
* indexer - http://www.logilab.org/project/indexer -
http://pypi.python.org/pypi/indexer - included in the forest
+* passlib - https://code.google.com/p/passlib/ -
+ http://pypi.python.org/pypi/passlib
+
To use network communication between cubicweb instances / clients:
* Pyro - http://www.xs4all.nl/~irmen/pyro3/ - http://pypi.python.org/pypi/Pyro
--- a/doc/book/en/annexes/faq.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/annexes/faq.rst Tue Oct 23 15:00:53 2012 +0200
@@ -364,7 +364,7 @@
>>> crypted = crypt_password('joepass')
>>> rset = rql('Any U WHERE U is CWUser, U login "joe"')
>>> joe = rset.get_entity(0,0)
- >>> joe.set_attributes(upassword=Binary(crypted))
+ >>> joe.cw_set(upassword=Binary(crypted))
Please, refer to the script example is provided in the `misc/examples/chpasswd.py` file.
--- a/doc/book/en/devrepo/entityclasses/application-logic.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/entityclasses/application-logic.rst Tue Oct 23 15:00:53 2012 +0200
@@ -38,7 +38,7 @@
object was built.
Setting an attribute or relation value can be done in the context of a
-Hook/Operation, using the obj.set_relations(x=42) notation or a plain
+Hook/Operation, using the obj.cw_set(x=42) notation or a plain
RQL SET expression.
In views, it would be preferable to encapsulate the necessary logic in
--- a/doc/book/en/devrepo/entityclasses/data-as-objects.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/entityclasses/data-as-objects.rst Tue Oct 23 15:00:53 2012 +0200
@@ -16,50 +16,47 @@
`Formatting and output generation`:
-* `view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
+* :meth:`view(__vid, __registry='views', **kwargs)`, applies the given view to the entity
(and returns an unicode string)
-* `absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url
+* :meth:`absolute_url(*args, **kwargs)`, returns an absolute URL including the base-url
-* `rest_path()`, returns a relative REST URL to get the entity
+* :meth:`rest_path()`, returns a relative REST URL to get the entity
-* `printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`,
+* :meth:`printable_value(attr, value=_marker, attrtype=None, format='text/html', displaytime=True)`,
returns a string enabling the display of an attribute value in a given format
(the value is automatically recovered if necessary)
`Data handling`:
-* `as_rset()`, converts the entity into an equivalent result set simulating the
+* :meth:`as_rset()`, converts the entity into an equivalent result set simulating the
request `Any X WHERE X eid _eid_`
-* `complete(skip_bytes=True)`, executes a request that recovers at
+* :meth:`complete(skip_bytes=True)`, executes a request that recovers at
once all the missing attributes of an entity
-* `get_value(name)`, returns the value associated to the attribute name given
+* :meth:`get_value(name)`, returns the value associated to the attribute name given
in parameter
-* `related(rtype, role='subject', limit=None, entities=False)`,
+* :meth:`related(rtype, role='subject', limit=None, entities=False)`,
returns a list of entities related to the current entity by the
relation given in parameter
-* `unrelated(rtype, targettype, role='subject', limit=None)`,
+* :meth:`unrelated(rtype, targettype, role='subject', limit=None)`,
returns a result set corresponding to the entities not (yet)
related to the current entity by the relation given in parameter
and satisfying its constraints
-* `set_attributes(**kwargs)`, updates the attributes list with the corresponding
- values given named parameters
+* :meth:`cw_set(**kwargs)`, updates entity's attributes and/or relation with the
+ corresponding values given named parameters. To set a relation where this
+ entity is the object of the relation, use `reverse_<relation>` as argument
+ name. Values may be an entity, a list of entities, or None (meaning that all
+ relations of the given type from or to this object should be deleted).
-* `set_relations(**kwargs)`, add relations to the given object. To
- set a relation where this entity is the object of the relation,
- use `reverse_<relation>` as argument name. Values may be an
- entity, a list of entities, or None (meaning that all relations of
- the given type from or to this object should be deleted).
-
-* `copy_relations(ceid)`, copies the relations of the entities having the eid
+* :meth:`copy_relations(ceid)`, copies the relations of the entities having the eid
given in the parameters on the current entity
-* `delete()` allows to delete the entity
+* :meth:`cw_delete()` allows to delete the entity
The :class:`AnyEntity` class
@@ -81,40 +78,30 @@
`Standard meta-data (Dublin Core)`:
-* `dc_title()`, returns a unicode string corresponding to the
+* :meth:`dc_title()`, returns a unicode string corresponding to the
meta-data `Title` (used by default is the first non-meta attribute
of the entity schema)
-* `dc_long_title()`, same as dc_title but can return a more
+* :meth:`dc_long_title()`, same as dc_title but can return a more
detailed title
-* `dc_description(format='text/plain')`, returns a unicode string
+* :meth:`dc_description(format='text/plain')`, returns a unicode string
corresponding to the meta-data `Description` (looks for a
description attribute by default)
-* `dc_authors()`, returns a unicode string corresponding to the meta-data
+* :meth:`dc_authors()`, returns a unicode string corresponding to the meta-data
`Authors` (owners by default)
-* `dc_creator()`, returns a unicode string corresponding to the
+* :meth:`dc_creator()`, returns a unicode string corresponding to the
creator of the entity
-* `dc_date(date_format=None)`, returns a unicode string corresponding to
+* :meth:`dc_date(date_format=None)`, returns a unicode string corresponding to
the meta-data `Date` (update date by default)
-* `dc_type(form='')`, returns a string to display the entity type by
+* :meth:`dc_type(form='')`, returns a string to display the entity type by
specifying the preferred form (`plural` for a plural form)
-* `dc_language()`, returns the language used by the entity
-
-
-`Misc methods`:
-
-* `after_deletion_path`, return (path, parameters) which should be
- used as redirect information when this entity is being deleted
-
-* `pre_web_edit`, callback called by the web editcontroller when an
- entity will be created/modified, to let a chance to do some entity
- specific stuff (does nothing by default)
+* :meth:`dc_language()`, returns the language used by the entity
Inheritance
-----------
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devrepo/fti.rst Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,159 @@
+.. _fti:
+
+Full Text Indexing in CubicWeb
+------------------------------
+
+When an attribute is tagged as *fulltext-indexable* in the datamodel,
+CubicWeb will automatically trigger hooks to update the internal
+fulltext index (i.e the ``appears`` SQL table) each time this attribute
+is modified.
+
+CubicWeb also provides a ``db-rebuild-fti`` command to rebuild the whole
+fulltext on demand:
+
+.. sourcecode:: bash
+
+ cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance
+
+You can also rebuild the fulltext index for a given set of entity types:
+
+.. sourcecode:: bash
+
+ cubicweb@esope~$ cubicweb db-rebuild-fti my_tracker_instance Ticket Version
+
+In the above example, only fulltext index of entity types ``Ticket`` and ``Version``
+will be rebuilt.
+
+
+Standard FTI process
+~~~~~~~~~~~~~~~~~~~~
+
+Considering an entity type ``ET``, the default *fti* process is to :
+
+1. fetch all entities of type ``ET``
+
+2. for each entity, adapt it to ``IFTIndexable`` (see
+ :class:`~cubicweb.entities.adapters.IFTIndexableAdapter`)
+
+3. call
+ :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words` on
+ the adapter which is supposed to return a dictionary *weight* ->
+ *list of words* as expected by
+ :meth:`~logilab.database.fti.FTIndexerMixIn.index_object`. The
+ tokenization of each attribute value is done by
+ :meth:`~logilab.database.fti.tokenize`.
+
+
+See :class:`~cubicweb.entities.adapters.IFTIndexableAdapter` for more documentation.
+
+
+Yams and ``fultext_container``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It is possible in the datamodel to indicate that fulltext-indexed
+attributes defined for an entity type will be used to index not the
+entity itself but a related entity. This is especially useful for
+composite entities. Let's take a look at (a simplified version of)
+the base schema defined in CubicWeb (see :mod:`cubicweb.schemas.base`):
+
+.. sourcecode:: python
+
+ class CWUser(WorkflowableEntityType):
+ login = String(required=True, unique=True, maxsize=64)
+ upassword = Password(required=True)
+
+ class EmailAddress(EntityType):
+ address = String(required=True, fulltextindexed=True,
+ indexed=True, unique=True, maxsize=128)
+
+
+ class use_email_relation(RelationDefinition):
+ name = 'use_email'
+ subject = 'CWUser'
+ object = 'EmailAddress'
+ cardinality = '*?'
+ composite = 'subject'
+
+
+The schema above states that there is a relation between ``CWUser`` and ``EmailAddress``
+and that the ``address`` field of ``EmailAddress`` is fulltext indexed. Therefore,
+in your application, if you use fulltext search to look for an email address, CubicWeb
+will return the ``EmailAddress`` itself. But the objects we'd like to index
+are more likely to be the associated ``CWUser`` than the ``EmailAddress`` itself.
+
+The simplest way to achieve that is to tag the ``use_email`` relation in
+the datamodel:
+
+.. sourcecode:: python
+
+ class use_email(RelationType):
+ fulltext_container = 'subject'
+
+
+Customizing how entities are fetched during ``db-rebuild-fti``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``db-rebuild-fti`` will call the
+:meth:`~cubicweb.entities.AnyEntity.cw_fti_index_rql_queries` class
+method on your entity type.
+
+.. automethod:: cubicweb.entities.AnyEntity.cw_fti_index_rql_queries
+
+Now, suppose you've got a _huge_ table to index, you probably don't want to
+get all entities at once. So here's a simple customized example that will
+process block of 10000 entities:
+
+.. sourcecode:: python
+
+
+ class MyEntityClass(AnyEntity):
+ __regid__ = 'MyEntityClass'
+
+ @classmethod
+ def cw_fti_index_rql_queries(cls, req):
+ # get the default RQL method and insert LIMIT / OFFSET instructions
+ base_rql = super(SearchIndex, cls).cw_fti_index_rql_queries(req)[0]
+ selected, restrictions = base_rql.split(' WHERE ')
+ rql_template = '%s ORDERBY X LIMIT %%(limit)s OFFSET %%(offset)s WHERE %s' % (
+ selected, restrictions)
+ # count how many entities you'll have to index
+ count = req.execute('Any COUNT(X) WHERE X is MyEntityClass')[0][0]
+ # iterate by blocks of 10000 entities
+ chunksize = 10000
+ for offset in xrange(0, count, chunksize):
+ print 'SENDING', rql_template % {'limit': chunksize, 'offset': offset}
+ yield rql_template % {'limit': chunksize, 'offset': offset}
+
+Since you have access to ``req``, you can more or less fetch whatever you want.
+
+
+Customizing :meth:`~cubicweb.entities.adapters.IFTIndexableAdapter.get_words`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can also customize the FTI process by providing your own ``get_words()``
+implementation:
+
+.. sourcecode:: python
+
+ from cubicweb.entities.adapters import IFTIndexableAdapter
+
+ class SearchIndexAdapter(IFTIndexableAdapter):
+ __regid__ = 'IFTIndexable'
+ __select__ = is_instance('MyEntityClass')
+
+ def fti_containers(self, _done=None):
+ """this should yield any entity that must be considered to
+ fulltext-index self.entity
+
+ CubicWeb's default implementation will look for yams'
+ ``fulltex_container`` property.
+ """
+ yield self.entity
+ yield self.entity.some_related_entity
+
+
+ def get_words(self):
+ # implement any logic here
+ # see http://www.postgresql.org/docs/9.1/static/textsearch-controls.html
+ # for the actual signification of 'C'
+ return {'C': ['any', 'word', 'I', 'want']}
--- a/doc/book/en/devrepo/index.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/index.rst Tue Oct 23 15:00:53 2012 +0200
@@ -21,3 +21,5 @@
testing.rst
migration.rst
profiling.rst
+ fti.rst
+
--- a/doc/book/en/devrepo/migration.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/migration.rst Tue Oct 23 15:00:53 2012 +0200
@@ -139,7 +139,7 @@
* `drop_relation_type(rtype, commit=True)`, removes a relation type and all the
definitions of this type.
-* `rename_relation(oldname, newname, commit=True)`, renames a relation.
+* `rename_relationi_type(oldname, newname, commit=True)`, renames a relation type.
* `add_relation_definition(subjtype, rtype, objtype, commit=True)`, adds a new
relation definition.
--- a/doc/book/en/devrepo/repo/hooks.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/repo/hooks.rst Tue Oct 23 15:00:53 2012 +0200
@@ -206,10 +206,11 @@
Reminder
~~~~~~~~
-You should never use the `entity.foo = 42` notation to update an
-entity. It will not do what you expect (updating the
-database). Instead, use the :meth:`set_attributes` and
-:meth:`set_relations` methods.
+You should never use the `entity.foo = 42` notation to update an entity. It will
+not do what you expect (updating the database). Instead, use the
+:meth:`~cubicweb.entity.Entity.cw_set` method or direct access to entity's
+:attr:`cw_edited` attribute if you're writing a hook for 'before_add_entity' or
+'before_update_entity' event.
How to choose between a before and an after event ?
--- a/doc/book/en/devrepo/testing.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devrepo/testing.rst Tue Oct 23 15:00:53 2012 +0200
@@ -70,13 +70,13 @@
def test_cannot_create_cycles(self):
# direct obvious cycle
- self.assertRaises(ValidationError, self.kw1.set_relations,
+ self.assertRaises(ValidationError, self.kw1.cw_set,
subkeyword_of=self.kw1)
# testing indirect cycles
kw3 = self.execute('INSERT Keyword SK: SK name "kwgroup2", SK included_in C, '
'SK subkeyword_of K WHERE C name "classif1", K eid %s'
% self.kw1.eid).get_entity(0,0)
- self.kw1.set_relations(subkeyword_of=kw3)
+ self.kw1.cw_set(subkeyword_of=kw3)
self.assertRaises(ValidationError, self.commit)
The test class defines a :meth:`setup_database` method which populates the
@@ -192,10 +192,10 @@
description=u'cubicweb is beautiful')
blog_entry_1 = req.create_entity('BlogEntry', title=u'hop',
content=u'cubicweb hop')
- blog_entry_1.set_relations(entry_of=cubicweb_blog)
+ blog_entry_1.cw_set(entry_of=cubicweb_blog)
blog_entry_2 = req.create_entity('BlogEntry', title=u'yes',
content=u'cubicweb yes')
- blog_entry_2.set_relations(entry_of=cubicweb_blog)
+ blog_entry_2.cw_set(entry_of=cubicweb_blog)
self.assertEqual(len(MAILBOX), 0)
self.commit()
self.assertEqual(len(MAILBOX), 2)
--- a/doc/book/en/devweb/ajax.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/ajax.rst Tue Oct 23 15:00:53 2012 +0200
@@ -7,6 +7,6 @@
You can, for instance, register some python functions that will become
callable from javascript through ajax calls. All the ajax URLs are handled
-by the ``AjaxController`` controller.
+by the :class:`cubicweb.web.views.ajaxcontroller.AjaxController` controller.
.. automodule:: cubicweb.web.views.ajaxcontroller
--- a/doc/book/en/devweb/index.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/index.rst Tue Oct 23 15:00:53 2012 +0200
@@ -10,6 +10,7 @@
publisher
controllers
request
+ searchbar
views/index
rtags
ajax
--- a/doc/book/en/devweb/property.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/property.rst Tue Oct 23 15:00:53 2012 +0200
@@ -1,3 +1,5 @@
+.. _cwprops:
+
The property mecanism
---------------------
--- a/doc/book/en/devweb/request.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/request.rst Tue Oct 23 15:00:53 2012 +0200
@@ -99,6 +99,7 @@
document.ready(...) or another ajax-friendly one-time trigger event
* `add_header(header, values)`: adds the header/value pair to the
current html headers
+ * `status_out`: control the HTTP status of the response
* `And more...`
--- a/doc/book/en/devweb/resource.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/resource.rst Tue Oct 23 15:00:53 2012 +0200
@@ -1,3 +1,5 @@
+.. _resources:
+
Locate resources
----------------
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devweb/searchbar.rst Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,41 @@
+.. _searchbar:
+
+RQL search bar
+--------------
+
+The RQL search bar is a visual component, hidden by default, the tiny *search*
+input being enough for common use cases.
+
+An autocompletion helper is provided to help you type valid queries, both
+in terms of syntax and in terms of schema validity.
+
+.. autoclass:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder
+
+
+How search is performed
++++++++++++++++++++++++
+
+You can use the *rql search bar* to either type RQL queries, plain text queries
+or standard shortcuts such as *<EntityType>* or *<EntityType> <attrname> <value>*.
+
+Ultimately, all queries are translated to rql since it's the only
+language understood on the server (data) side. To transform the user
+query into RQL, CubicWeb uses the so-called *magicsearch component*,
+defined in :mod:`cubicweb.web.views.magicsearch`, which in turn
+delegates to a number of query preprocessor that are responsible of
+interpreting the user query and generating corresponding RQL.
+
+The code of the main processor loop is easy to understand:
+
+.. sourcecode:: python
+
+ for proc in self.processors:
+ try:
+ return proc.process_query(uquery, req)
+ except (RQLSyntaxError, BadRQLQuery):
+ pass
+
+The idea is simple: for each query processor, try to translate the
+query. If it fails, try with the next processor, if it succeeds,
+we're done and the RQL query will be executed.
+
--- a/doc/book/en/devweb/views/index.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/views/index.rst Tue Oct 23 15:00:53 2012 +0200
@@ -18,10 +18,11 @@
boxes
table
xmlrss
-.. editforms
urlpublish
breadcrumbs
idownloadable
wdoc
+
+.. editforms
.. embedding
--- a/doc/book/en/devweb/views/views.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/devweb/views/views.rst Tue Oct 23 15:00:53 2012 +0200
@@ -32,33 +32,10 @@
Basic class for views
~~~~~~~~~~~~~~~~~~~~~
-Class `View` (`cubicweb.view`)
-```````````````````````````````
-
-This class is an abstraction of a view class, used as a base class for
-every renderable object such as views, templates and other user
-interface components.
-
-A `View` is instantiated to render a result set or part of a result
-set. `View` subclasses may be parametrized using the following class
-attributes:
+Class :class:`~cubicweb.view.View`
+``````````````````````````````````
-* `templatable` indicates if the view may be embedded in a main
- template or if it has to be rendered standalone (i.e. pure XML views
- must not be embedded in the main template of HTML pages)
-
-* if the view is not templatable, it should set the `content_type`
- class attribute to the correct MIME type (text/xhtml being the
- default)
-
-* the `category` attribute may be used in the interface to regroup
- related view kinds together
-
-A view writes to its output stream thanks to its attribute `w` (the
-append method of an `UStreamIO`, except for binary views).
-
-At instantiation time, the standard `_cw` and `cw_rset` attributes are
-added and the `w` attribute will be set at rendering time.
+.. autoclass:: cubicweb.view.View
The basic interface for views is as follows (remember that the result
set has a tabular structure with rows and columns, hence cells):
@@ -88,12 +65,13 @@
Other basic view classes
````````````````````````
-Here are some of the subclasses of `View` defined in `cubicweb.view`
+Here are some of the subclasses of :class:`~cubicweb.view.View` defined in :mod:`cubicweb.view`
that are more concrete as they relate to data rendering within the application:
-* `EntityView`, view applying to lines or cell containing an entity (e.g. an eid)
-* `StartupView`, start view that does not require a result set to apply to
-* `AnyRsetView`, view applicable to any result set
+.. autoclass:: cubicweb.view.EntityView
+.. autoclass:: cubicweb.view.StartupView
+.. autoclass:: cubicweb.view.EntityStartupView
+.. autoclass:: cubicweb.view.AnyRsetView
Examples of views class
```````````````````````
--- a/doc/book/en/tutorials/advanced/part02_security.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/tutorials/advanced/part02_security.rst Tue Oct 23 15:00:53 2012 +0200
@@ -196,7 +196,7 @@
for eid in self.get_data():
entity = self.session.entity_from_eid(eid)
if entity.visibility == 'parent':
- entity.set_attributes(visibility=u'authenticated')
+ entity.cw_set(visibility=u'authenticated')
class SetVisibilityHook(hook.Hook):
__regid__ = 'sytweb.setvisibility'
@@ -215,7 +215,7 @@
parent = self._cw.entity_from_eid(self.eidto)
child = self._cw.entity_from_eid(self.eidfrom)
if child.visibility == 'parent':
- child.set_attributes(visibility=parent.visibility)
+ child.cw_set(visibility=parent.visibility)
Notice:
@@ -344,7 +344,7 @@
self.assertEquals(len(req.execute('Folder X')), 0) # restricted...
# may_be_read_by propagation
self.restore_connection()
- folder.set_relations(may_be_read_by=toto)
+ folder.cw_set(may_be_read_by=toto)
self.commit()
photo1.clear_all_caches()
self.failUnless(photo1.may_be_read_by)
--- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst Tue Oct 23 15:00:53 2012 +0200
@@ -294,6 +294,7 @@
folder in which the current file (e.g. `self.entity`) is located.
.. Note::
+
The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default
:class:`IBreadCrumbsAdapter` provides a default implementation for it that will look
at the value returned by its `parent_entity` method. It also provides a
@@ -331,6 +332,7 @@
navigate through the web site to see if everything is ok...
.. Note::
+
In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while
in the two other, it refers to the **instance** (if you can't see the
difference, reread CubicWeb's concept chapter !).
@@ -363,4 +365,4 @@
.. _`several improvments`: http://www.cubicweb.org/blogentry/1179899
.. _`3.8`: http://www.cubicweb.org/blogentry/917107
.. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642
-.. _`an earlier post`: http://www.cubicweb.org/867464
\ No newline at end of file
+.. _`an earlier post`: http://www.cubicweb.org/867464
--- a/entities/__init__.py Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/__init__.py Tue Oct 23 15:00:53 2012 +0200
@@ -40,6 +40,24 @@
""" return the url of the entity creation form for this entity type"""
return req.build_url('add/%s' % cls.__regid__, **kwargs)
+ @classmethod
+ def cw_fti_index_rql_queries(cls, req):
+ """return the list of rql queries to fetch entities to FT-index
+
+ The default is to fetch all entities at once and to prefetch
+ indexable attributes but one could imagine iterating over
+ "smaller" resultsets if the table is very big or returning
+ a subset of entities that match some business-logic condition.
+ """
+ restrictions = ['X is %s' % cls.__regid__]
+ selected = ['X']
+ for attrschema in cls.e_schema.indexable_attributes():
+ varname = attrschema.type.upper()
+ restrictions.append('X %s %s' % (attrschema, varname))
+ selected.append(varname)
+ return ['Any %s WHERE %s' % (', '.join(selected),
+ ', '.join(restrictions))]
+
# meta data api ###########################################################
def dc_title(self):
--- a/entities/adapters.py Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/adapters.py Tue Oct 23 15:00:53 2012 +0200
@@ -87,10 +87,20 @@
class IFTIndexableAdapter(view.EntityAdapter):
+ """standard adapter to handle fulltext indexing
+
+ .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.fti_containers
+ .. automethod:: cubicweb.entities.adapters.IFTIndexableAdapter.get_words
+ """
__regid__ = 'IFTIndexable'
__select__ = is_instance('Any')
def fti_containers(self, _done=None):
+ """return the list of entities to index when handling ``self.entity``
+
+ The actual list of entities depends on ``fulltext_container`` usage
+ in the datamodel definition
+ """
if _done is None:
_done = set()
entity = self.entity
--- a/entities/authobjs.py Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/authobjs.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -77,6 +77,19 @@
self._properties = dict((p.pkey, p.value) for p in self.reverse_for_user)
return self._properties
+ def prefered_language(self, language=None):
+ """return language used by this user, if explicitly defined (eg not
+ using http negociation)
+ """
+ language = language or self.property_value('ui.language')
+ vreg = self._cw.vreg
+ try:
+ vreg.config.translations[language]
+ except KeyError:
+ language = vreg.property_value('ui.language')
+ assert language in vreg.config.translations[language], language
+ return language
+
def property_value(self, key):
try:
# properties stored on the user aren't correctly typed
@@ -101,7 +114,7 @@
kwargs['for_user'] = self
self._cw.create_entity('CWProperty', **kwargs)
else:
- prop.set_attributes(value=value)
+ prop.cw_set(value=value)
def matching_groups(self, groups):
"""return the number of the given group(s) in which the user is
--- a/entities/sources.py Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/sources.py Tue Oct 23 15:00:53 2012 +0200
@@ -51,7 +51,7 @@
continue
raise
cfgstr = unicode(generate_source_config(sconfig), self._cw.encoding)
- self.set_attributes(config=cfgstr)
+ self.cw_set(config=cfgstr)
class CWSource(_CWSourceCfgMixIn, AnyEntity):
@@ -181,5 +181,5 @@
def write_log(self, session, **kwargs):
if 'status' not in kwargs:
kwargs['status'] = getattr(self, '_status', u'success')
- self.set_attributes(log=u'<br/>'.join(self._logs), **kwargs)
+ self.cw_set(log=u'<br/>'.join(self._logs), **kwargs)
self._logs = []
--- a/entities/test/unittest_base.py Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/test/unittest_base.py Tue Oct 23 15:00:53 2012 +0200
@@ -19,6 +19,7 @@
"""unit tests for cubicweb.entities.base module
"""
+from __future__ import with_statement
from logilab.common.testlib import unittest_main
from logilab.common.decorators import clear_cache
@@ -57,13 +58,19 @@
self.assertEqual(dict((str(k), v) for k, v in self.schema['State'].meta_attributes().iteritems()),
{'description_format': ('format', 'description')})
+ def test_fti_rql_method(self):
+ eclass = self.vreg['etypes'].etype_class('EmailAddress')
+ self.assertEqual(['Any X, ALIAS, ADDRESS WHERE X is EmailAddress, '
+ 'X alias ALIAS, X address ADDRESS'],
+ eclass.cw_fti_index_rql_queries(self.request()))
+
class EmailAddressTC(BaseEntityTC):
def test_canonical_form(self):
email1 = self.execute('INSERT EmailAddress X: X address "maarten.ter.huurne@philips.com"').get_entity(0, 0)
email2 = self.execute('INSERT EmailAddress X: X address "maarten@philips.com"').get_entity(0, 0)
email3 = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr"').get_entity(0, 0)
- email1.set_relations(prefered_form=email2)
+ email1.cw_set(prefered_form=email2)
self.assertEqual(email1.prefered.eid, email2.eid)
self.assertEqual(email2.prefered.eid, email2.eid)
self.assertEqual(email3.prefered.eid, email3.eid)
@@ -97,10 +104,10 @@
e = self.execute('CWUser U WHERE U login "member"').get_entity(0, 0)
self.assertEqual(e.dc_title(), 'member')
self.assertEqual(e.name(), 'member')
- e.set_attributes(firstname=u'bouah')
+ e.cw_set(firstname=u'bouah')
self.assertEqual(e.dc_title(), 'member')
self.assertEqual(e.name(), u'bouah')
- e.set_attributes(surname=u'lôt')
+ e.cw_set(surname=u'lôt')
self.assertEqual(e.dc_title(), 'member')
self.assertEqual(e.name(), u'bouah lôt')
--- a/entities/test/unittest_wfobjs.py Wed Feb 22 11:57:42 2012 +0100
+++ b/entities/test/unittest_wfobjs.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -20,7 +20,6 @@
from cubicweb import ValidationError
from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.server.session import security_enabled
def add_wf(self, etype, name=None, default=False):
@@ -63,7 +62,7 @@
# gnark gnark
bar = wf.add_state(u'bar')
self.commit()
- bar.set_attributes(name=u'foo')
+ bar.cw_set(name=u'foo')
with self.assertRaises(ValidationError) as cm:
self.commit()
self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'})
@@ -86,7 +85,7 @@
# gnark gnark
biz = wf.add_transition(u'biz', (bar,), foo)
self.commit()
- biz.set_attributes(name=u'baz')
+ biz.cw_set(name=u'baz')
with self.assertRaises(ValidationError) as cm:
self.commit()
self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'})
@@ -126,8 +125,9 @@
self.assertEqual(trs[0].destination(None).name, u'deactivated')
# test a std user get no possible transition
cnx = self.login('member')
+ req = self.request()
# fetch the entity using the new session
- trs = list(cnx.user().cw_adapt_to('IWorkflowable').possible_transitions())
+ trs = list(req.user.cw_adapt_to('IWorkflowable').possible_transitions())
self.assertEqual(len(trs), 0)
cnx.close()
@@ -154,7 +154,7 @@
wf = add_wf(self, 'CWUser')
s = wf.add_state(u'foo', initial=True)
self.commit()
- with security_enabled(self.session, write=False):
+ with self.session.security_enabled(write=False):
with self.assertRaises(ValidationError) as cm:
self.session.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
{'x': self.user().eid, 's': s.eid})
@@ -173,7 +173,7 @@
def test_goback_transition(self):
req = self.request()
- wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow
+ wf = req.user.cw_adapt_to('IWorkflowable').current_workflow
asleep = wf.add_state('asleep')
wf.add_transition('rest', (wf.state_by_name('activated'),
wf.state_by_name('deactivated')),
@@ -212,7 +212,7 @@
req = self.request()
iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
iworkflowable.fire_transition('deactivate')
- cnx.commit()
+ req.cu.commit()
with self.assertRaises(ValidationError) as cm:
iworkflowable.fire_transition('activate')
self.assertEqual(cm.exception.errors, {'by_transition-subject': "transition may not be fired"})
@@ -516,7 +516,7 @@
['rest'])
self.assertEqual(parse_hist(iworkflowable.workflow_history),
[('asleep', 'asleep', 'rest', None)])
- user.set_attributes(surname=u'toto') # fulfill condition
+ user.cw_set(surname=u'toto') # fulfill condition
self.commit()
iworkflowable.fire_transition('rest')
self.commit()
@@ -556,13 +556,12 @@
def setUp(self):
CubicWebTC.setUp(self)
- self.wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow
- self.session.set_cnxset()
+ req = self.request()
+ self.wf = req.user.cw_adapt_to('IWorkflowable').current_workflow
self.s_activated = self.wf.state_by_name('activated').eid
self.s_deactivated = self.wf.state_by_name('deactivated').eid
self.s_dummy = self.wf.add_state(u'dummy').eid
self.wf.add_transition(u'dummy', (self.s_deactivated,), self.s_dummy)
- req = self.request()
ueid = self.create_user(req, 'stduser', commit=False).eid
# test initial state is set
rset = self.execute('Any N WHERE S name N, X in_state S, X eid %(x)s',
--- a/entity.py Wed Feb 22 11:57:42 2012 +0100
+++ b/entity.py Tue Oct 23 15:00:53 2012 +0200
@@ -452,26 +452,13 @@
return mainattr, needcheck
@classmethod
- def cw_instantiate(cls, execute, **kwargs):
- """add a new entity of this given type
-
- Example (in a shell session):
-
- >>> companycls = vreg['etypes'].etype_class(('Company')
- >>> personcls = vreg['etypes'].etype_class(('Person')
- >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
- >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
- ... works_for=c)
-
- You can also set relation where the entity has 'object' role by
- prefixing the relation by 'reverse_'.
- """
- rql = 'INSERT %s X' % cls.__regid__
+ def _cw_build_entity_query(cls, kwargs):
relations = []
restrictions = set()
- pending_relations = []
+ pendingrels = []
eschema = cls.e_schema
qargs = {}
+ attrcache = {}
for attr, value in kwargs.items():
if attr.startswith('reverse_'):
attr = attr[len('reverse_'):]
@@ -487,10 +474,13 @@
value = iter(value).next()
else:
# prepare IN clause
- pending_relations.append( (attr, role, value) )
+ pendingrels.append( (attr, role, value) )
continue
if rschema.final: # attribute
relations.append('X %s %%(%s)s' % (attr, attr))
+ attrcache[attr] = value
+ elif value is None:
+ pendingrels.append( (attr, role, value) )
else:
rvar = attr.upper()
if role == 'object':
@@ -503,19 +493,52 @@
if hasattr(value, 'eid'):
value = value.eid
qargs[attr] = value
+ rql = u''
if relations:
- rql = '%s: %s' % (rql, ', '.join(relations))
+ rql += ', '.join(relations)
if restrictions:
- rql = '%s WHERE %s' % (rql, ', '.join(restrictions))
- created = execute(rql, qargs).get_entity(0, 0)
- for attr, role, values in pending_relations:
+ rql += ' WHERE %s' % ', '.join(restrictions)
+ return rql, qargs, pendingrels, attrcache
+
+ @classmethod
+ def _cw_handle_pending_relations(cls, eid, pendingrels, execute):
+ for attr, role, values in pendingrels:
if role == 'object':
restr = 'Y %s X' % attr
else:
restr = 'X %s Y' % attr
+ if values is None:
+ execute('DELETE %s WHERE X eid %%(x)s' % restr, {'x': eid})
+ continue
execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
restr, ','.join(str(getattr(r, 'eid', r)) for r in values)),
- {'x': created.eid}, build_descr=False)
+ {'x': eid}, build_descr=False)
+
+ @classmethod
+ def cw_instantiate(cls, execute, **kwargs):
+ """add a new entity of this given type
+
+ Example (in a shell session):
+
+ >>> companycls = vreg['etypes'].etype_class(('Company')
+ >>> personcls = vreg['etypes'].etype_class(('Person')
+ >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
+ >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
+ ... works_for=c)
+
+ You can also set relations where the entity has 'object' role by
+ prefixing the relation name by 'reverse_'. Also, relation values may be
+ an entity or eid, a list of entities or eids.
+ """
+ rql, qargs, pendingrels, attrcache = cls._cw_build_entity_query(kwargs)
+ if rql:
+ rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
+ else:
+ rql = 'INSERT %s X' % (cls.__regid__)
+ created = execute(rql, qargs).get_entity(0, 0)
+ created._cw_update_attr_cache(attrcache)
+ created.cw_attr_cache.update(attrcache)
+ cls._cw_handle_pending_relations(created.eid, pendingrels, execute)
return created
def __init__(self, req, rset=None, row=None, col=0):
@@ -535,6 +558,24 @@
def __cmp__(self, other):
raise NotImplementedError('comparison not implemented for %s' % self.__class__)
+ def _cw_update_attr_cache(self, attrcache):
+ # if context is a repository session, don't consider dont-cache-attrs as
+ # the instance already hold modified values and loosing them could
+ # introduce severe problems
+ if self._cw.is_request:
+ for attr in self._cw.get_shared_data('%s.dont-cache-attrs' % self.eid,
+ default=(), txdata=True, pop=True):
+ attrcache.pop(attr, None)
+ self.cw_attr_cache.pop(attr, None)
+ self.cw_attr_cache.update(attrcache)
+
+ def _cw_dont_cache_attribute(self, attr):
+ """repository side method called when some attribute have been
+ transformed by a hook, hence original value should not be cached by
+ client
+ """
+ self._cw.transaction_data.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr)
+
def __json_encode__(self):
"""custom json dumps hook to dump the entity's eid
which is not part of dict structure itself
@@ -635,11 +676,12 @@
mainattr, needcheck = self.cw_rest_attr_info()
etype = str(self.e_schema)
path = etype.lower()
+ fallback = False
if mainattr != 'eid':
value = getattr(self, mainattr)
if not can_use_rest_path(value):
mainattr = 'eid'
- path += '/eid'
+ path = None
elif needcheck:
# make sure url is not ambiguous
try:
@@ -650,12 +692,16 @@
nbresults = self.__unique = self._cw.execute(rql, {'value' : value})[0][0]
if nbresults != 1: # ambiguity?
mainattr = 'eid'
- path += '/eid'
+ path = None
if mainattr == 'eid':
if use_ext_eid:
value = self.cw_metainformation()['extid']
else:
value = self.eid
+ if path is None:
+ # fallback url: <base-url>/<eid> url is used as cw entities uri,
+ # prefer it to <base-url>/<etype>/eid/<eid>
+ return unicode(value)
return '%s/%s' % (path, self._cw.url_quote(value))
def cw_attr_metadata(self, attr, metadata):
@@ -1107,6 +1153,9 @@
# insert security RQL expressions granting the permission to 'add' the
# relation into the rql syntax tree, if necessary
rqlexprs = rdef.get_rqlexprs('add')
+ if not self.has_eid():
+ rqlexprs = [rqlexpr for rqlexpr in rqlexprs
+ if searchedvar.name in rqlexpr.mainvars]
if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
# compute a varmap suitable to RQLRewriter.rewrite argument
varmap = dict((v, v) for v in (searchedvar.name, evar.name)
@@ -1207,54 +1256,41 @@
# raw edition utilities ###################################################
- def set_attributes(self, **kwargs): # XXX cw_set_attributes
+ def cw_set(self, **kwargs):
+ """update this entity using given attributes / relation, working in the
+ same fashion as :meth:`cw_instantiate`.
+
+ Example (in a shell session):
+
+ >>> c = rql('Any X WHERE X is Company').get_entity(0, 0)
+ >>> p = rql('Any X WHERE X is Person').get_entity(0, 0)
+ >>> c.set(name=u'Logilab')
+ >>> p.set(firstname=u'John', lastname=u'Doe', works_for=c)
+
+ You can also set relations where the entity has 'object' role by
+ prefixing the relation name by 'reverse_'. Also, relation values may be
+ an entity or eid, a list of entities or eids, or None (meaning that all
+ relations of the given type from or to this object should be deleted).
+ """
_check_cw_unsafe(kwargs)
assert kwargs
assert self.cw_is_saved(), "should not call set_attributes while entity "\
"hasn't been saved yet"
- relations = ['X %s %%(%s)s' % (key, key) for key in kwargs]
- # and now update the database
- kwargs['x'] = self.eid
- self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
- kwargs)
- kwargs.pop('x')
+ rql, qargs, pendingrels, attrcache = self._cw_build_entity_query(kwargs)
+ if rql:
+ rql = 'SET ' + rql
+ qargs['x'] = self.eid
+ if ' WHERE ' in rql:
+ rql += ', X eid %(x)s'
+ else:
+ rql += ' WHERE X eid %(x)s'
+ self._cw.execute(rql, qargs)
# update current local object _after_ the rql query to avoid
# interferences between the query execution itself and the cw_edited /
# skip_security machinery
- self.cw_attr_cache.update(kwargs)
-
- def set_relations(self, **kwargs): # XXX cw_set_relations
- """add relations to the given object. To set a relation where this entity
- is the object of the relation, use 'reverse_'<relation> as argument name.
-
- Values may be an entity or eid, a list of entities or eids, or None
- (meaning that all relations of the given type from or to this object
- should be deleted).
- """
- # XXX update cache
- _check_cw_unsafe(kwargs)
- for attr, values in kwargs.iteritems():
- if attr.startswith('reverse_'):
- restr = 'Y %s X' % attr[len('reverse_'):]
- else:
- restr = 'X %s Y' % attr
- if values is None:
- self._cw.execute('DELETE %s WHERE X eid %%(x)s' % restr,
- {'x': self.eid})
- continue
- if not isinstance(values, (tuple, list, set, frozenset)):
- values = (values,)
- eids = []
- for val in values:
- try:
- eids.append(str(val.eid))
- except AttributeError:
- try:
- eids.append(str(typed_eid(val)))
- except (ValueError, TypeError):
- raise Exception('expected an Entity or eid, got %s' % val)
- self._cw.execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
- restr, ','.join(eids)), {'x': self.eid})
+ self._cw_update_attr_cache(attrcache)
+ self._cw_handle_pending_relations(self.eid, pendingrels, self._cw.execute)
+ # XXX update relation cache
def cw_delete(self, **kwargs):
assert self.has_eid(), self.eid
@@ -1269,6 +1305,21 @@
# deprecated stuff #########################################################
+ @deprecated('[3.16] use cw_set() instead')
+ def set_attributes(self, **kwargs): # XXX cw_set_attributes
+ self.cw_set(**kwargs)
+
+ @deprecated('[3.16] use cw_set() instead')
+ def set_relations(self, **kwargs): # XXX cw_set_relations
+ """add relations to the given object. To set a relation where this entity
+ is the object of the relation, use 'reverse_'<relation> as argument name.
+
+ Values may be an entity or eid, a list of entities or eids, or None
+ (meaning that all relations of the given type from or to this object
+ should be deleted).
+ """
+ self.cw_set(**kwargs)
+
@deprecated('[3.13] use entity.cw_clear_all_caches()')
def clear_all_caches(self):
return self.cw_clear_all_caches()
--- a/etwist/http.py Wed Feb 22 11:57:42 2012 +0100
+++ b/etwist/http.py Tue Oct 23 15:00:53 2012 +0200
@@ -43,19 +43,3 @@
def __repr__(self):
return "<%s.%s code=%d>" % (self.__module__, self.__class__.__name__, self._code)
-
-
-def not_modified_response(twisted_request, headers_in):
- headers_out = Headers()
-
- for header in (
- # Required from sec 10.3.5:
- 'date', 'etag', 'content-location', 'expires',
- 'cache-control', 'vary',
- # Others:
- 'server', 'proxy-authenticate', 'www-authenticate', 'warning'):
- value = headers_in.getRawHeaders(header)
- if value is not None:
- headers_out.setRawHeaders(header, value)
- return HTTPResponse(twisted_request=twisted_request,
- headers=headers_out)
--- a/etwist/request.py Wed Feb 22 11:57:42 2012 +0100
+++ b/etwist/request.py Tue Oct 23 15:00:53 2012 +0200
@@ -27,27 +27,18 @@
from cubicweb.web.request import CubicWebRequestBase
from cubicweb.web.httpcache import GMTOFFSET
from cubicweb.web.http_headers import Headers
-from cubicweb.etwist.http import not_modified_response
class CubicWebTwistedRequestAdapter(CubicWebRequestBase):
- def __init__(self, req, vreg, https, base_url):
+ def __init__(self, req, vreg, https):
self._twreq = req
- self._base_url = base_url
- super(CubicWebTwistedRequestAdapter, self).__init__(vreg, https, req.args)
+ super(CubicWebTwistedRequestAdapter, self).__init__(
+ vreg, https, req.args, headers=req.received_headers)
for key, (name, stream) in req.files.iteritems():
if name is None:
self.form[key] = (name, stream)
else:
self.form[key] = (unicode(name, self.encoding), stream)
- # XXX can't we keep received_headers?
- self._headers_in = Headers()
- for k, v in req.received_headers.iteritems():
- self._headers_in.addRawHeader(k, v)
-
- def base_url(self):
- """return the root url of the instance"""
- return self._base_url
def http_method(self):
"""returns 'POST', 'GET', 'HEAD', etc."""
@@ -65,56 +56,3 @@
if not includeparams:
path = path.split('?', 1)[0]
return path
-
- def get_header(self, header, default=None, raw=True):
- """return the value associated with the given input header, raise
- KeyError if the header is not set
- """
- if raw:
- return self._headers_in.getRawHeaders(header, [default])[0]
- return self._headers_in.getHeader(header, default)
-
- def _validate_cache(self):
- """raise a `DirectResponse` exception if a cached page along the way
- exists and is still usable
- """
- if self.get_header('Cache-Control') in ('max-age=0', 'no-cache'):
- # Expires header seems to be required by IE7
- self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
- return
- # when using both 'Last-Modified' and 'ETag' response headers
- # (i.e. using respectively If-Modified-Since and If-None-Match request
- # headers, see
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4 for
- # reference
- last_modified = self.headers_out.getHeader('last-modified')
- if last_modified is not None:
- status = self._twreq.setLastModified(last_modified)
- if status != http.CACHED:
- return
- etag = self.headers_out.getRawHeaders('etag')
- if etag is not None:
- status = self._twreq.setETag(etag[0])
- if status == http.CACHED:
- response = not_modified_response(self._twreq, self._headers_in)
- raise DirectResponse(response)
- # Expires header seems to be required by IE7
- self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-
- def header_accept_language(self):
- """returns an ordered list of preferred languages"""
- acceptedlangs = self.get_header('Accept-Language', raw=False) or {}
- for lang, _ in sorted(acceptedlangs.iteritems(), key=lambda x: x[1],
- reverse=True):
- lang = lang.split('-')[0]
- yield lang
-
- def header_if_modified_since(self):
- """If the HTTP header If-modified-since is set, return the equivalent
- date time value (GMT), else return None
- """
- mtime = self.get_header('If-modified-since', raw=False)
- if mtime:
- # :/ twisted is returned a localized time stamp
- return datetime.fromtimestamp(mtime) + GMTOFFSET
- return None
--- a/etwist/server.py Wed Feb 22 11:57:42 2012 +0100
+++ b/etwist/server.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -48,7 +48,7 @@
from cubicweb import (AuthenticationError, ConfigurationError,
CW_EVENT_MANAGER, CubicWebException)
from cubicweb.utils import json_dumps
-from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
+from cubicweb.web import DirectResponse
from cubicweb.web.application import CubicWebPublisher
from cubicweb.web.http_headers import generateDateTime
from cubicweb.etwist.request import CubicWebTwistedRequestAdapter
@@ -57,7 +57,7 @@
def start_task(interval, func):
lc = task.LoopingCall(func)
# wait until interval has expired to actually start the task, else we have
- # to wait all task to be finished for the server to be actually started
+ # to wait all tasks to be finished for the server to be actually started
lc.start(interval, now=False)
def host_prefixed_baseurl(baseurl, host):
@@ -69,166 +69,6 @@
return baseurl
-class ForbiddenDirectoryLister(resource.Resource):
- def render(self, request):
- return HTTPResponse(twisted_request=request,
- code=http.FORBIDDEN,
- stream='Access forbidden')
-
-
-class NoListingFile(static.File):
- def __init__(self, config, path=None):
- if path is None:
- path = config.static_directory
- static.File.__init__(self, path)
- self.config = config
-
- def set_expires(self, request):
- if not self.config.debugmode:
- # XXX: Don't provide additional resource information to error responses
- #
- # the HTTP RFC recommands not going further than 1 year ahead
- expires = date.today() + timedelta(days=6*30)
- request.setHeader('Expires', generateDateTime(mktime(expires.timetuple())))
-
- def directoryListing(self):
- return ForbiddenDirectoryLister()
-
-
-class DataLookupDirectory(NoListingFile):
- def __init__(self, config, path):
- self.md5_version = config.instance_md5_version()
- NoListingFile.__init__(self, config, path)
- self.here = path
- self._defineChildResources()
- if self.config.debugmode:
- self.data_modconcat_basepath = '/data/??'
- else:
- self.data_modconcat_basepath = '/data/%s/??' % self.md5_version
-
- def _defineChildResources(self):
- self.putChild(self.md5_version, self)
-
- def getChild(self, path, request):
- if not path:
- uri = request.uri
- if uri.startswith('/https/'):
- uri = uri[6:]
- if uri.startswith(self.data_modconcat_basepath):
- resource_relpath = uri[len(self.data_modconcat_basepath):]
- if resource_relpath:
- paths = resource_relpath.split(',')
- try:
- self.set_expires(request)
- return ConcatFiles(self.config, paths)
- except ConcatFileNotFoundError:
- return self.childNotFound
- return self.directoryListing()
- childpath = join(self.here, path)
- dirpath, rid = self.config.locate_resource(childpath)
- if dirpath is None:
- # resource not found
- return self.childNotFound
- filepath = os.path.join(dirpath, rid)
- if os.path.isdir(filepath):
- resource = DataLookupDirectory(self.config, childpath)
- # cache resource for this segment path to avoid recomputing
- # directory lookup
- self.putChild(path, resource)
- return resource
- else:
- self.set_expires(request)
- return NoListingFile(self.config, filepath)
-
-
-class FCKEditorResource(NoListingFile):
-
- def getChild(self, path, request):
- pre_path = request.path.split('/')[1:]
- if pre_path[0] == 'https':
- pre_path.pop(0)
- uiprops = self.config.https_uiprops
- else:
- uiprops = self.config.uiprops
- return static.File(osp.join(uiprops['FCKEDITOR_PATH'], path))
-
-
-class LongTimeExpiringFile(DataLookupDirectory):
- """overrides static.File and sets a far future ``Expires`` date
- on the resouce.
-
- versions handling is done by serving static files by different
- URLs for each version. For instance::
-
- http://localhost:8080/data-2.48.2/cubicweb.css
- http://localhost:8080/data-2.49.0/cubicweb.css
- etc.
-
- """
- def _defineChildResources(self):
- pass
-
-
-class ConcatFileNotFoundError(CubicWebException):
- pass
-
-
-class ConcatFiles(LongTimeExpiringFile):
- def __init__(self, config, paths):
- _, ext = osp.splitext(paths[0])
- self._resources = {}
- # create a unique / predictable filename. We don't consider cubes
- # version since uicache is cleared at server startup, and file's dates
- # are checked in debug mode
- fname = 'cache_concat_' + md5(';'.join(paths)).hexdigest() + ext
- filepath = osp.join(config.appdatahome, 'uicache', fname)
- LongTimeExpiringFile.__init__(self, config, filepath)
- self._concat_cached_filepath(filepath, paths)
-
- def _resource(self, path):
- try:
- return self._resources[path]
- except KeyError:
- self._resources[path] = self.config.locate_resource(path)
- return self._resources[path]
-
- def _concat_cached_filepath(self, filepath, paths):
- if not self._up_to_date(filepath, paths):
- with open(filepath, 'wb') as f:
- for path in paths:
- dirpath, rid = self._resource(path)
- if rid is None:
- # In production mode log an error, do not return a 404
- # XXX the erroneous content is cached anyway
- LOGGER.error('concatenated data url error: %r file '
- 'does not exist', path)
- if self.config.debugmode:
- raise ConcatFileNotFoundError(path)
- else:
- for line in open(osp.join(dirpath, rid)):
- f.write(line)
- f.write('\n')
-
- def _up_to_date(self, filepath, paths):
- """
- The concat-file is considered up-to-date if it exists.
- In debug mode, an additional check is performed to make sure that
- concat-file is more recent than all concatenated files
- """
- if not osp.isfile(filepath):
- return False
- if self.config.debugmode:
- concat_lastmod = os.stat(filepath).st_mtime
- for path in paths:
- dirpath, rid = self._resource(path)
- if rid is None:
- raise ConcatFileNotFoundError(path)
- path = osp.join(dirpath, rid)
- if os.stat(path).st_mtime > concat_lastmod:
- return False
- return True
-
-
class CubicWebRootResource(resource.Resource):
def __init__(self, config, vreg=None):
resource.Resource.__init__(self)
@@ -240,9 +80,6 @@
self.https_url = config['https-url']
global MAX_POST_LENGTH
MAX_POST_LENGTH = config['max-post-length']
- self.putChild('static', NoListingFile(config))
- self.putChild('fckeditor', FCKEditorResource(self.config, ''))
- self.putChild('data', DataLookupDirectory(self.config, ''))
def init_publisher(self):
config = self.config
@@ -320,88 +157,28 @@
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
- baseurl = self.https_url or self.base_url
- else:
- https = False
- baseurl = self.base_url
- if self.config['use-request-subdomain']:
- baseurl = host_prefixed_baseurl(baseurl, host)
- self.warning('used baseurl is %s for this request', baseurl)
- req = CubicWebTwistedRequestAdapter(request, self.appli.vreg, https, baseurl)
- if req.authmode == 'http':
- # activate realm-based auth
- realm = self.config['realm']
- req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
- try:
- self.appli.connect(req)
- except Redirect, ex:
- return self.redirect(request=req, location=ex.location)
- if https and req.session.anonymous_session and self.config['https-deny-anonymous']:
- # don't allow anonymous on https connection
- return self.request_auth(request=req)
if self.url_rewriter is not None:
# XXX should occur before authentication?
- try:
- path = self.url_rewriter.rewrite(host, origpath, req)
- except Redirect, ex:
- return self.redirect(req, ex.location)
+ path = self.url_rewriter.rewrite(host, origpath, request)
request.uri.replace(origpath, path, 1)
else:
path = origpath
- if not path or path == "/":
- path = 'view'
+ req = CubicWebTwistedRequestAdapter(request, self.appli.vreg, https)
try:
- result = self.appli.publish(path, req)
+ ### Try to generate the actual request content
+ content = self.appli.handle_request(req, path)
except DirectResponse, ex:
return ex.response
- except StatusResponse, ex:
- return HTTPResponse(stream=ex.content, code=ex.status,
- twisted_request=req._twreq,
- headers=req.headers_out)
- except AuthenticationError:
- return self.request_auth(request=req)
- except LogOut, ex:
- if self.config['auth-mode'] == 'cookie' and ex.url:
- return self.redirect(request=req, location=ex.url)
- # in http we have to request auth to flush current http auth
- # information
- return self.request_auth(request=req, loggedout=True)
- except Redirect, ex:
- return self.redirect(request=req, location=ex.location)
- # request may be referenced by "onetime callback", so clear its entity
- # cache to avoid memory usage
- req.drop_entity_cache()
- return HTTPResponse(twisted_request=req._twreq, code=http.OK,
- stream=result, headers=req.headers_out)
-
- def redirect(self, request, location):
- self.debug('redirecting to %s', str(location))
- request.headers_out.setHeader('location', str(location))
- # 303 See other
- return HTTPResponse(twisted_request=request._twreq, code=303,
- headers=request.headers_out)
-
- def request_auth(self, request, loggedout=False):
- if self.https_url and request.base_url() != self.https_url:
- return self.redirect(request, self.https_url + 'login')
- if self.config['auth-mode'] == 'http':
- code = http.UNAUTHORIZED
- else:
- code = http.FORBIDDEN
- if loggedout:
- if request.https:
- request._base_url = self.base_url
- request.https = False
- content = self.appli.loggedout_content(request)
- else:
- content = self.appli.need_login_content(request)
- return HTTPResponse(twisted_request=request._twreq,
- stream=content, code=code,
- headers=request.headers_out)
+ # at last: create twisted object
+ return HTTPResponse(code = req.status_out,
+ headers = req.headers_out,
+ stream = content,
+ twisted_request=req._twreq)
# these are overridden by set_log_methods below
# only defining here to prevent pylint from complaining
--- a/etwist/test/unittest_server.py Wed Feb 22 11:57:42 2012 +0100
+++ b/etwist/test/unittest_server.py Tue Oct 23 15:00:53 2012 +0200
@@ -19,8 +19,7 @@
import os, os.path as osp, glob
from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.etwist.server import (host_prefixed_baseurl, ConcatFiles,
- ConcatFileNotFoundError)
+from cubicweb.etwist.server import host_prefixed_baseurl
class HostPrefixedBaseURLTC(CubicWebTC):
@@ -54,30 +53,6 @@
self._check('http://localhost:8080/hg/', 'code.cubicweb.org',
'http://localhost:8080/hg/')
-
-class ConcatFilesTC(CubicWebTC):
-
- def tearDown(self):
- super(ConcatFilesTC, self).tearDown()
- self._cleanup_concat_cache()
- self.config.debugmode = False
-
- def _cleanup_concat_cache(self):
- uicachedir = osp.join(self.config.apphome, 'uicache')
- for fname in glob.glob(osp.join(uicachedir, 'cache_concat_*')):
- os.unlink(osp.join(uicachedir, fname))
-
- def test_cache(self):
- concat = ConcatFiles(self.config, ('cubicweb.ajax.js', 'jquery.js'))
- self.assertTrue(osp.isfile(concat.path))
-
- def test_404(self):
- # when not in debug mode, should not crash
- ConcatFiles(self.config, ('cubicweb.ajax.js', 'dummy.js'))
- # in debug mode, raise error
- self.config.debugmode = True
- try:
- self.assertRaises(ConcatFileNotFoundError, ConcatFiles, self.config,
- ('cubicweb.ajax.js', 'dummy.js'))
- finally:
- self.config.debugmode = False
+if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
+ unittest_main()
--- a/ext/tal.py Wed Feb 22 11:57:42 2012 +0100
+++ b/ext/tal.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -261,7 +261,7 @@
return wrapped
def _compiled_template(self, instance):
- for fileordirectory in instance.config.vregistry_path():
+ for fileordirectory in instance.config.appobjects_path():
filepath = join(fileordirectory, self.filename)
if isdir(fileordirectory) and exists(filepath):
return compile_template_file(filepath)
--- a/ext/test/unittest_rest.py Wed Feb 22 11:57:42 2012 +0100
+++ b/ext/test/unittest_rest.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,9 +15,6 @@
#
# 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 unittest_main
from cubicweb.devtools.testlib import CubicWebTC
@@ -60,23 +57,23 @@
def test_rql_role_with_vid(self):
context = self.context()
out = rest_publish(context, ':rql:`Any X WHERE X is CWUser:table`')
- self.assert_(out.endswith('<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a>'
- '</td></tr></tbody></table></div>\n</div>\n</p>\n'))
+ self.assertTrue(out.endswith('<a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a>'
+ '</td></tr>\n</tbody></table></div></p>\n'))
def test_rql_role_with_vid_empty_rset(self):
context = self.context()
out = rest_publish(context, ':rql:`Any X WHERE X is CWUser, X login "nono":table`')
- self.assert_(out.endswith('<p><div class="searchMessage"><strong>No result matching query</strong></div>\n</p>\n'))
+ self.assertTrue(out.endswith('<p><div class="searchMessage"><strong>No result matching query</strong></div>\n</p>\n'))
def test_rql_role_with_unknown_vid(self):
context = self.context()
out = rest_publish(context, ':rql:`Any X WHERE X is CWUser:toto`')
- self.assert_(out.startswith("<p>an error occured while interpreting this rql directive: ObjectNotFound(u'toto',)</p>"))
+ self.assertTrue(out.startswith("<p>an error occured while interpreting this rql directive: ObjectNotFound(u'toto',)</p>"))
def test_rql_role_without_vid(self):
context = self.context()
out = rest_publish(context, ':rql:`Any X WHERE X is CWUser`')
- self.assertEqual(out, u'<p><h1>cwuser_plural</h1><div class="section"><a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div><div class="section"><a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div></p>\n')
+ self.assertEqual(out, u'<p><h1>CWUser_plural</h1><div class="section"><a href="http://testing.fr/cubicweb/cwuser/admin" title="">admin</a></div><div class="section"><a href="http://testing.fr/cubicweb/cwuser/anon" title="">anon</a></div></p>\n')
if __name__ == '__main__':
unittest_main()
--- a/hooks/__init__.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/__init__.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -46,7 +46,7 @@
session.commit()
finally:
session.close()
- if self.repo.config['undo-support']:
+ if self.repo.config['undo-enabled']:
self.repo.looping_task(60*60*24, cleanup_old_transactions,
self.repo)
def update_feeds(repo):
--- a/hooks/bookmark.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/bookmark.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/hooks/email.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/email.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,9 +15,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/>.
-"""hooks to ensure use_email / primary_email relations consistency
+"""hooks to ensure use_email / primary_email relations consistency"""
-"""
__docformat__ = "restructuredtext en"
from cubicweb.server import hook
--- a/hooks/integrity.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/integrity.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -20,12 +20,11 @@
"""
__docformat__ = "restructuredtext en"
+_ = unicode
from threading import Lock
-from yams.schema import role_name
-
-from cubicweb import ValidationError
+from cubicweb import validation_error
from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES,
RQLConstraint, RQLUniqueConstraint)
from cubicweb.predicates import is_instance
@@ -87,11 +86,11 @@
continue
if not session.execute(self.base_rql % rtype, {'x': eid}):
etype = session.describe(eid)[0]
- _ = session._
msg = _('at least one relation %(rtype)s is required on '
'%(etype)s (%(eid)s)')
- msg %= {'rtype': _(rtype), 'etype': _(etype), 'eid': eid}
- raise ValidationError(eid, {role_name(rtype, self.role): msg})
+ raise validation_error(eid, {(rtype, self.role): msg},
+ {'rtype': rtype, 'etype': etype, 'eid': eid},
+ ['rtype', 'etype'])
class _CheckSRelationOp(_CheckRequiredRelationOperation):
@@ -231,9 +230,9 @@
rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
rset = self._cw.execute(rql, {'val': val})
if rset and rset[0][0] != entity.eid:
- msg = self._cw._('the value "%s" is already used, use another one')
- qname = role_name(attr, 'subject')
- raise ValidationError(entity.eid, {qname: msg % val})
+ msg = _('the value "%s" is already used, use another one')
+ raise validation_error(entity, {(attr, 'subject'): msg},
+ (val,))
class DontRemoveOwnersGroupHook(IntegrityHook):
@@ -246,15 +245,12 @@
def __call__(self):
entity = self.entity
if self.event == 'before_delete_entity' and entity.name == 'owners':
- msg = self._cw._('can\'t be deleted')
- raise ValidationError(entity.eid, {None: msg})
+ raise validation_error(entity, {None: _("can't be deleted")})
elif self.event == 'before_update_entity' \
and 'name' in entity.cw_edited:
oldname, newname = entity.cw_edited.oldnewvalue('name')
if oldname == 'owners' and newname != oldname:
- qname = role_name('name', 'subject')
- msg = self._cw._('can\'t be changed')
- raise ValidationError(entity.eid, {qname: msg})
+ raise validation_error(entity, {('name', 'subject'): _("can't be changed")})
class TidyHtmlFields(IntegrityHook):
@@ -301,11 +297,10 @@
def precommit_event(self):
session = self.session
pendingeids = session.transaction_data.get('pendingeids', ())
- neweids = session.transaction_data.get('neweids', ())
eids_by_etype_rtype = {}
for eid, rtype in self.get_data():
- # don't do anything if the entity is being created or deleted
- if not (eid in pendingeids or eid in neweids):
+ # don't do anything if the entity is being deleted
+ if eid not in pendingeids:
etype = session.describe(eid)[0]
key = (etype, rtype)
if key not in eids_by_etype_rtype:
--- a/hooks/metadata.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/metadata.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -199,17 +199,12 @@
entity = self._cw.entity_from_eid(self.eidfrom)
# copy entity if necessary
if not oldsource.repo_source.copy_based_source:
- entity.complete(skip_bytes=False)
+ entity.complete(skip_bytes=False, skip_pwd=False)
if not entity.creation_date:
entity.cw_attr_cache['creation_date'] = datetime.now()
if not entity.modification_date:
entity.cw_attr_cache['modification_date'] = datetime.now()
entity.cw_attr_cache['cwuri'] = u'%s%s' % (self._cw.base_url(), entity.eid)
- for rschema, attrschema in entity.e_schema.attribute_definitions():
- if attrschema == 'Password' and \
- rschema.rdef(entity.e_schema, attrschema).cardinality[0] == '1':
- from logilab.common.shellutils import generate_password
- entity.cw_attr_cache[rschema.type] = generate_password()
entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
syssource.add_entity(self._cw, entity)
# we don't want the moved entity to be reimported later. To
--- a/hooks/syncschema.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/syncschema.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -24,6 +24,7 @@
"""
__docformat__ = "restructuredtext en"
+_ = unicode
from copy import copy
from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema
@@ -31,7 +32,7 @@
from logilab.common.decorators import clear_cache
-from cubicweb import ValidationError
+from cubicweb import validation_error
from cubicweb.predicates import is_instance
from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
CONSTRAINTS, ETYPE_NAME_MAP, display_name)
@@ -127,10 +128,9 @@
if attr in ro_attrs:
origval, newval = entity.cw_edited.oldnewvalue(attr)
if newval != origval:
- errors[attr] = session._("can't change the %s attribute") % \
- display_name(session, attr)
+ errors[attr] = _("can't change this attribute")
if errors:
- raise ValidationError(entity.eid, errors)
+ raise validation_error(entity, errors)
class _MockEntity(object): # XXX use a named tuple with python 2.6
@@ -755,7 +755,13 @@
cols = ['%s%s' % (prefix, c) for c in self.cols]
sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols)
for sql in sqls:
- session.system_sql(sql)
+ try:
+ session.system_sql(sql)
+ except Exception, exc: # should be ProgrammingError
+ if sql.startswith('DROP'):
+ self.error('execute of `%s` failed (cause: %s)', sql, exc)
+ continue
+ raise
# XXX revertprecommit_event
@@ -907,7 +913,7 @@
# final entities can't be deleted, don't care about that
name = self.entity.name
if name in CORE_TYPES:
- raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
+ raise validation_error(self.entity, {None: _("can't be deleted")})
# delete every entities of this type
if name not in ETYPE_NAME_MAP:
self._cw.execute('DELETE %s X' % name)
@@ -977,7 +983,7 @@
def __call__(self):
name = self.entity.name
if name in CORE_TYPES:
- raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
+ raise validation_error(self.entity, {None: _("can't be deleted")})
# delete relation definitions using this relation type
self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
{'x': self.entity.eid})
--- a/hooks/syncsession.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/syncsession.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -18,9 +18,9 @@
"""Core hooks: synchronize living session on persistent data changes"""
__docformat__ = "restructuredtext en"
+_ = unicode
-from yams.schema import role_name
-from cubicweb import UnknownProperty, ValidationError, BadConnectionId
+from cubicweb import UnknownProperty, BadConnectionId, validation_error
from cubicweb.predicates import is_instance
from cubicweb.server import hook
@@ -165,13 +165,11 @@
try:
value = session.vreg.typed_value(key, value)
except UnknownProperty:
- qname = role_name('pkey', 'subject')
- msg = session._('unknown property key %s') % key
- raise ValidationError(self.entity.eid, {qname: msg})
+ msg = _('unknown property key %s')
+ raise validation_error(self.entity, {('pkey', 'subject'): msg}, (key,))
except ValueError, ex:
- qname = role_name('value', 'subject')
- raise ValidationError(self.entity.eid,
- {qname: session._(str(ex))})
+ raise validation_error(self.entity,
+ {('value', 'subject'): str(ex)})
if not session.user.matching_groups('managers'):
session.add_relation(self.entity.eid, 'for_user', session.user.eid)
else:
@@ -196,8 +194,7 @@
except UnknownProperty:
return
except ValueError, ex:
- qname = role_name('value', 'subject')
- raise ValidationError(entity.eid, {qname: session._(str(ex))})
+ raise validation_error(entity, {('value', 'subject'): str(ex)})
if entity.for_user:
for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
_ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
@@ -237,10 +234,8 @@
key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
{'x': eidfrom})[0]
if session.vreg.property_info(key)['sitewide']:
- qname = role_name('for_user', 'subject')
- msg = session._("site-wide property can't be set for user")
- raise ValidationError(eidfrom,
- {qname: msg})
+ msg = _("site-wide property can't be set for user")
+ raise validation_error(eidfrom, {('for_user', 'subject'): msg})
for session_ in get_user_sessions(session.repo, self.eidto):
_ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
key=key, value=value)
--- a/hooks/syncsources.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/syncsources.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2010-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -17,12 +17,13 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""hooks for repository sources synchronization"""
+_ = unicode
+
from socket import gethostname
from logilab.common.decorators import clear_cache
-from yams.schema import role_name
-from cubicweb import ValidationError
+from cubicweb import validation_error
from cubicweb.predicates import is_instance
from cubicweb.server import SOURCE_TYPES, hook
@@ -46,12 +47,15 @@
try:
sourcecls = SOURCE_TYPES[self.entity.type]
except KeyError:
- msg = self._cw._('unknown source type')
- raise ValidationError(self.entity.eid,
- {role_name('type', 'subject'): msg})
- sourcecls.check_conf_dict(self.entity.eid, self.entity.host_config,
- fail_if_unknown=not self._cw.vreg.config.repairing)
- SourceAddedOp(self._cw, entity=self.entity)
+ msg = _('Unknown source type')
+ raise validation_error(self.entity, {('type', 'subject'): msg})
+ # ignore creation of the system source done during database
+ # initialisation, as config for this source is in a file and handling
+ # is done separatly (no need for the operation either)
+ if self.entity.name != 'system':
+ sourcecls.check_conf_dict(self.entity.eid, self.entity.host_config,
+ fail_if_unknown=not self._cw.vreg.config.repairing)
+ SourceAddedOp(self._cw, entity=self.entity)
class SourceRemovedOp(hook.Operation):
@@ -65,7 +69,8 @@
events = ('before_delete_entity',)
def __call__(self):
if self.entity.name == 'system':
- raise ValidationError(self.entity.eid, {None: 'cant remove system source'})
+ msg = _("You cannot remove the system source")
+ raise validation_error(self.entity, {None: msg})
SourceRemovedOp(self._cw, uri=self.entity.name)
@@ -116,11 +121,18 @@
__select__ = SourceHook.__select__ & is_instance('CWSource')
events = ('before_update_entity',)
def __call__(self):
- if 'config' in self.entity.cw_edited:
- SourceConfigUpdatedOp.get_instance(self._cw).add_data(self.entity)
if 'name' in self.entity.cw_edited:
oldname, newname = self.entity.cw_edited.oldnewvalue('name')
+ if oldname == 'system':
+ msg = _("You cannot rename the system source")
+ raise validation_error(self.entity, {('name', 'subject'): msg})
SourceRenamedOp(self._cw, oldname=oldname, newname=newname)
+ if 'config' in self.entity.cw_edited:
+ if self.entity.name == 'system' and self.entity.config:
+ msg = _("Configuration of the system source goes to "
+ "the 'sources' file, not in the database")
+ raise validation_error(self.entity, {('config', 'subject'): msg})
+ SourceConfigUpdatedOp.get_instance(self._cw).add_data(self.entity)
class SourceHostConfigUpdatedHook(SourceHook):
@@ -154,8 +166,8 @@
events = ('before_add_relation',)
def __call__(self):
if not self._cw.added_in_transaction(self.eidfrom):
- msg = self._cw._("can't change this relation")
- raise ValidationError(self.eidfrom, {self.rtype: msg})
+ msg = _("You can't change this relation")
+ raise validation_error(self.eidfrom, {self.rtype: msg})
class SourceMappingChangedOp(hook.DataOperationMixIn, hook.Operation):
--- a/hooks/test/unittest_hooks.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/test/unittest_hooks.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -67,10 +67,9 @@
entity = self.request().create_entity('Workflow', name=u'wf1',
description_format=u'text/html',
description=u'yo')
- entity.set_attributes(name=u'wf2')
+ entity.cw_set(name=u'wf2')
self.assertEqual(entity.description, u'yo')
- entity.set_attributes(description=u'R&D<p>yo')
- entity.cw_attr_cache.pop('description')
+ entity.cw_set(description=u'R&D<p>yo')
self.assertEqual(entity.description, u'R&D<p>yo</p>')
def test_metadata_cwuri(self):
@@ -171,6 +170,7 @@
try:
self.execute('INSERT CWUser X: X login "admin"')
except ValidationError, ex:
+ ex.tr(unicode)
self.assertIsInstance(ex.entity, int)
self.assertEqual(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
--- a/hooks/test/unittest_syncschema.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/test/unittest_syncschema.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -294,7 +294,7 @@
def test_change_fulltext_container(self):
req = self.request()
target = req.create_entity(u'EmailAddress', address=u'rick.roll@dance.com')
- target.set_relations(reverse_use_email=req.user)
+ target.cw_set(reverse_use_email=req.user)
self.commit()
rset = req.execute('Any X WHERE X has_text "rick.roll"')
self.assertIn(req.user.eid, [item[0] for item in rset])
--- a/hooks/test/unittest_syncsession.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/test/unittest_syncsession.py Tue Oct 23 15:00:53 2012 +0200
@@ -31,9 +31,11 @@
def test_unexistant_cwproperty(self):
with self.assertRaises(ValidationError) as cm:
self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
+ cm.exception.tr(unicode)
self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
with self.assertRaises(ValidationError) as cm:
self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
+ cm.exception.tr(unicode)
self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
def test_site_wide_cwproperty(self):
--- a/hooks/workflow.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/workflow.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -18,12 +18,12 @@
"""Core hooks: workflow related hooks"""
__docformat__ = "restructuredtext en"
+_ = unicode
from datetime import datetime
-from yams.schema import role_name
-from cubicweb import RepositoryError, ValidationError
+from cubicweb import RepositoryError, validation_error
from cubicweb.predicates import is_instance, adaptable
from cubicweb.server import hook
@@ -92,9 +92,8 @@
if mainwf.eid == self.wfeid:
deststate = mainwf.initial
if not deststate:
- qname = role_name('custom_workflow', 'subject')
- msg = session._('workflow has no initial state')
- raise ValidationError(entity.eid, {qname: msg})
+ msg = _('workflow has no initial state')
+ raise validation_error(entity, {('custom_workflow', 'subject'): msg})
if mainwf.state_by_eid(iworkflowable.current_state.eid):
# nothing to do
return
@@ -119,9 +118,8 @@
outputs = set()
for ep in tr.subworkflow_exit:
if ep.subwf_state.eid in outputs:
- qname = role_name('subworkflow_exit', 'subject')
- msg = self.session._("can't have multiple exits on the same state")
- raise ValidationError(self.treid, {qname: msg})
+ msg = _("can't have multiple exits on the same state")
+ raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg})
outputs.add(ep.subwf_state.eid)
@@ -137,13 +135,12 @@
wftr = iworkflowable.subworkflow_input_transition()
if wftr is None:
# inconsistency detected
- qname = role_name('to_state', 'subject')
- msg = session._("state doesn't belong to entity's current workflow")
- raise ValidationError(self.trinfo.eid, {'to_state': msg})
+ msg = _("state doesn't belong to entity's current workflow")
+ raise validation_error(self.trinfo, {('to_state', 'subject'): msg})
tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
if tostate is not None:
# reached an exit point
- msg = session._('exiting from subworkflow %s')
+ msg = _('exiting from subworkflow %s')
msg %= session._(iworkflowable.current_workflow.name)
session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
@@ -186,9 +183,8 @@
try:
foreid = entity.cw_attr_cache['wf_info_for']
except KeyError:
- qname = role_name('wf_info_for', 'subject')
- msg = session._('mandatory relation')
- raise ValidationError(entity.eid, {qname: msg})
+ msg = _('mandatory relation')
+ raise validation_error(entity, {('wf_info_for', 'subject'): msg})
forentity = session.entity_from_eid(foreid)
# see comment in the TrInfo entity definition
entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for)
@@ -201,13 +197,13 @@
else:
wf = iworkflowable.current_workflow
if wf is None:
- msg = session._('related entity has no workflow set')
- raise ValidationError(entity.eid, {None: msg})
+ msg = _('related entity has no workflow set')
+ raise validation_error(entity, {None: msg})
# then check it has a state set
fromstate = iworkflowable.current_state
if fromstate is None:
- msg = session._('related entity has no state')
- raise ValidationError(entity.eid, {None: msg})
+ msg = _('related entity has no state')
+ raise validation_error(entity, {None: msg})
# True if we are coming back from subworkflow
swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
cowpowers = (session.user.is_in_group('managers')
@@ -219,47 +215,42 @@
# no transition set, check user is a manager and destination state
# is specified (and valid)
if not cowpowers:
- qname = role_name('by_transition', 'subject')
- msg = session._('mandatory relation')
- raise ValidationError(entity.eid, {qname: msg})
+ msg = _('mandatory relation')
+ raise validation_error(entity, {('by_transition', 'subject'): msg})
deststateeid = entity.cw_attr_cache.get('to_state')
if not deststateeid:
- qname = role_name('by_transition', 'subject')
- msg = session._('mandatory relation')
- raise ValidationError(entity.eid, {qname: msg})
+ msg = _('mandatory relation')
+ raise validation_error(entity, {('by_transition', 'subject'): msg})
deststate = wf.state_by_eid(deststateeid)
if deststate is None:
- qname = role_name('to_state', 'subject')
- msg = session._("state doesn't belong to entity's workflow")
- raise ValidationError(entity.eid, {qname: msg})
+ msg = _("state doesn't belong to entity's workflow")
+ raise validation_error(entity, {('to_state', 'subject'): msg})
else:
# check transition is valid and allowed, unless we're coming back
# from subworkflow
tr = session.entity_from_eid(treid)
if swtr is None:
- qname = role_name('by_transition', 'subject')
+ qname = ('by_transition', 'subject')
if tr is None:
- msg = session._("transition doesn't belong to entity's workflow")
- raise ValidationError(entity.eid, {qname: msg})
+ msg = _("transition doesn't belong to entity's workflow")
+ raise validation_error(entity, {qname: msg})
if not tr.has_input_state(fromstate):
- msg = session._("transition %(tr)s isn't allowed from %(st)s") % {
- 'tr': session._(tr.name), 'st': session._(fromstate.name)}
- raise ValidationError(entity.eid, {qname: msg})
+ msg = _("transition %(tr)s isn't allowed from %(st)s")
+ raise validation_error(entity, {qname: msg}, {
+ 'tr': tr.name, 'st': fromstate.name}, ['tr', 'st'])
if not tr.may_be_fired(foreid):
- msg = session._("transition may not be fired")
- raise ValidationError(entity.eid, {qname: msg})
+ msg = _("transition may not be fired")
+ raise validation_error(entity, {qname: msg})
deststateeid = entity.cw_attr_cache.get('to_state')
if deststateeid is not None:
if not cowpowers and deststateeid != tr.destination(forentity).eid:
- qname = role_name('by_transition', 'subject')
- msg = session._("transition isn't allowed")
- raise ValidationError(entity.eid, {qname: msg})
+ msg = _("transition isn't allowed")
+ raise validation_error(entity, {('by_transition', 'subject'): msg})
if swtr is None:
deststate = session.entity_from_eid(deststateeid)
if not cowpowers and deststate is None:
- qname = role_name('to_state', 'subject')
- msg = session._("state doesn't belong to entity's workflow")
- raise ValidationError(entity.eid, {qname: msg})
+ msg = _("state doesn't belong to entity's workflow")
+ raise validation_error(entity, {('to_state', 'subject'): msg})
else:
deststateeid = tr.destination(forentity).eid
# everything is ok, add missing information on the trinfo entity
@@ -307,20 +298,18 @@
iworkflowable = entity.cw_adapt_to('IWorkflowable')
mainwf = iworkflowable.main_workflow
if mainwf is None:
- msg = session._('entity has no workflow set')
- raise ValidationError(entity.eid, {None: msg})
+ msg = _('entity has no workflow set')
+ raise validation_error(entity, {None: msg})
for wf in mainwf.iter_workflows():
if wf.state_by_eid(self.eidto):
break
else:
- qname = role_name('in_state', 'subject')
- msg = session._("state doesn't belong to entity's workflow. You may "
- "want to set a custom workflow for this entity first.")
- raise ValidationError(self.eidfrom, {qname: msg})
+ msg = _("state doesn't belong to entity's workflow. You may "
+ "want to set a custom workflow for this entity first.")
+ raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
- qname = role_name('in_state', 'subject')
- msg = session._("state doesn't belong to entity's current workflow")
- raise ValidationError(self.eidfrom, {qname: msg})
+ msg = _("state doesn't belong to entity's current workflow")
+ raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
class SetModificationDateOnStateChange(WorkflowHook):
@@ -335,7 +324,7 @@
return
entity = self._cw.entity_from_eid(self.eidfrom)
try:
- entity.set_attributes(modification_date=datetime.now())
+ entity.cw_set(modification_date=datetime.now())
except RepositoryError, ex:
# usually occurs if entity is coming from a read-only source
# (eg ldap user)
--- a/hooks/zmq.py Wed Feb 22 11:57:42 2012 +0100
+++ b/hooks/zmq.py Tue Oct 23 15:00:53 2012 +0200
@@ -46,3 +46,30 @@
self.repo.app_instances_bus.add_subscriber(address)
self.repo.app_instances_bus.start()
+
+class ZMQRepositoryServerStopHook(hook.Hook):
+ __regid__ = 'zmqrepositoryserverstop'
+ events = ('server_shutdown',)
+
+ def __call__(self):
+ server = getattr(self.repo, 'zmq_repo_server', None)
+ if server:
+ self.repo.zmq_repo_server.quit()
+
+class ZMQRepositoryServerStartHook(hook.Hook):
+ __regid__ = 'zmqrepositoryserverstart'
+ events = ('server_startup',)
+
+ def __call__(self):
+ config = self.repo.config
+ if config.name == 'repository':
+ # start-repository command already starts a zmq repo
+ return
+ address = config.get('zmq-repository-address')
+ if not address:
+ return
+ from cubicweb.server import cwzmq
+ self.repo.zmq_repo_server = server = cwzmq.ZMQRepositoryServer(self.repo)
+ server.connect(address)
+ self.repo.threaded_task(server.run)
+
--- a/i18n/de.po Wed Feb 22 11:57:42 2012 +0100
+++ b/i18n/de.po Tue Oct 23 15:00:53 2012 +0200
@@ -145,6 +145,10 @@
msgid "(UNEXISTANT EID)"
msgstr "(EID nicht gefunden)"
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr ""
+
msgid "**"
msgstr "0..n 0..n"
@@ -218,6 +222,10 @@
msgid "About this site"
msgstr "Ãœber diese Seite"
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
msgid "Any"
msgstr "irgendein"
@@ -263,6 +271,10 @@
msgid "Browse by entity type"
msgstr "nach Identitätstyp navigieren"
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr ""
+
msgid "Bytes"
msgstr "Bytes"
@@ -390,14 +402,6 @@
#, python-format
msgid ""
-"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
-"exists anymore in the schema."
-msgstr ""
-"Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, "
-"diese Relation existiert nicht mehr in dem Schema."
-
-#, python-format
-msgid ""
"Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
"anymore."
msgstr ""
@@ -423,6 +427,10 @@
msgid "Click to sort on this column"
msgstr ""
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
msgid "DEBUG"
msgstr ""
@@ -448,6 +456,14 @@
msgid "Decimal_plural"
msgstr "Dezimalzahlen"
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr ""
+
msgid "Detected problems"
msgstr ""
@@ -872,12 +888,22 @@
msgid "URLs from which content will be imported. You can put one url per line"
msgstr ""
+msgid "Undoable actions"
+msgstr ""
+
+msgid "Undoing"
+msgstr ""
+
msgid "UniqueConstraint"
msgstr "eindeutige Einschränkung"
msgid "Unreachable objects"
msgstr "unzugängliche Objekte"
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr ""
+
msgid "Used by:"
msgstr "benutzt von:"
@@ -975,9 +1001,6 @@
msgid "abstract base class for transitions"
msgstr "abstrakte Basisklasse für Übergänge"
-msgid "action menu"
-msgstr ""
-
msgid "action(s) on this selection"
msgstr "Aktionen(en) bei dieser Auswahl"
@@ -1265,6 +1288,9 @@
msgid "bad value"
msgstr "Unzulässiger Wert"
+msgid "badly formatted url"
+msgstr ""
+
msgid "base url"
msgstr "Basis-URL"
@@ -1349,6 +1375,9 @@
msgid "can not resolve entity types:"
msgstr "Die Typen konnten nicht ermittelt werden:"
+msgid "can only have one url"
+msgstr ""
+
msgid "can't be changed"
msgstr "kann nicht geändert werden"
@@ -1386,6 +1415,22 @@
#, python-format
msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+
+#, python-format
+msgid ""
"can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
"%(card)s"
msgstr ""
@@ -2015,9 +2060,6 @@
msgid "date"
msgstr "Datum"
-msgid "day"
-msgstr ""
-
msgid "deactivate"
msgstr "deaktivieren"
@@ -2519,6 +2561,9 @@
msgid "foaf"
msgstr "FOAF"
+msgid "focus on this selection"
+msgstr ""
+
msgid "follow"
msgstr "dem Link folgen"
@@ -2672,6 +2717,9 @@
msgid "has_text"
msgstr "enthält Text"
+msgid "header-center"
+msgstr ""
+
msgid "header-left"
msgstr ""
@@ -3031,9 +3079,6 @@
msgid "log in"
msgstr "anmelden"
-msgid "log out first"
-msgstr "Melden Sie sich zuerst ab."
-
msgid "login"
msgstr "Anmeldung"
@@ -3136,9 +3181,6 @@
msgid "monday"
msgstr "Montag"
-msgid "month"
-msgstr ""
-
msgid "more actions"
msgstr "weitere Aktionen"
@@ -3241,6 +3283,9 @@
msgid "new"
msgstr "neu"
+msgid "next page"
+msgstr ""
+
msgid "next_results"
msgstr "weitere Ergebnisse"
@@ -3442,6 +3487,9 @@
msgid "preferences"
msgstr "Einstellungen"
+msgid "previous page"
+msgstr ""
+
msgid "previous_results"
msgstr "vorige Ergebnisse"
@@ -4028,6 +4076,10 @@
msgid "there is no previous page"
msgstr ""
+#, python-format
+msgid "there is no transaction #%s"
+msgstr ""
+
msgid "this action is not reversible!"
msgstr "Achtung! Diese Aktion ist unumkehrbar."
@@ -4111,15 +4163,15 @@
msgid "to_state_object"
msgstr "Ãœbergang zu diesem Zustand"
-msgid "today"
-msgstr ""
-
msgid "todo_by"
msgstr "zu erledigen bis"
msgid "toggle check boxes"
msgstr "Kontrollkästchen umkehren"
+msgid "toggle filter"
+msgstr "filter verbergen/zeigen"
+
msgid "tr_count"
msgstr ""
@@ -4238,6 +4290,9 @@
msgid "unauthorized value"
msgstr "ungültiger Wert"
+msgid "undefined user"
+msgstr ""
+
msgid "undo"
msgstr "rückgängig machen"
@@ -4265,6 +4320,9 @@
msgid "unknown vocabulary:"
msgstr "Unbekanntes Wörterbuch : "
+msgid "unsupported protocol"
+msgstr ""
+
msgid "upassword"
msgstr "Passwort"
@@ -4474,9 +4532,6 @@
msgid "wednesday"
msgstr "Mittwoch"
-msgid "week"
-msgstr "Woche"
-
#, python-format
msgid "welcome %s !"
msgstr "Willkommen %s !"
@@ -4573,45 +4628,9 @@
msgid "you should un-inline relation %s which is supported and may be crossed "
msgstr ""
-#~ msgid "(loading ...)"
-#~ msgstr "(laden...)"
-
-#~ msgid "Schema of the data model"
-#~ msgstr "Schema des Datenmodells"
-
-#~ msgid "csv entities export"
-#~ msgstr "CSV-Export von Entitäten"
-
-#~ msgid "follow this link if javascript is deactivated"
-#~ msgstr "Folgen Sie diesem Link, falls Javascript deaktiviert ist."
-
#~ msgid ""
-#~ "how to format date and time in the ui (\"man strftime\" for format "
-#~ "description)"
+#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does "
+#~ "not exists anymore in the schema."
#~ msgstr ""
-#~ "Wie formatiert man das Datum Interface im (\"man strftime\" für die "
-#~ "Beschreibung des neuen Formats"
-
-#~ msgid ""
-#~ "how to format date in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "Wie formatiert man das Datum im Interface (\"man strftime\" für die "
-#~ "Beschreibung des Formats)"
-
-#~ msgid ""
-#~ "how to format time in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "Wie man die Uhrzeit im Interface (\"man strftime\" für die "
-#~ "Formatbeschreibung)"
-
-#~ msgid "instance schema"
-#~ msgstr "Schema der Instanz"
-
-#~ msgid "rss"
-#~ msgstr "RSS"
-
-#~ msgid "xbel"
-#~ msgstr "XBEL"
-
-#~ msgid "xml"
-#~ msgstr "XML"
+#~ "Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, "
+#~ "diese Relation existiert nicht mehr in dem Schema."
--- a/i18n/en.po Wed Feb 22 11:57:42 2012 +0100
+++ b/i18n/en.po Tue Oct 23 15:00:53 2012 +0200
@@ -137,6 +137,10 @@
msgid "(UNEXISTANT EID)"
msgstr ""
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr ""
+
msgid "**"
msgstr "0..n 0..n"
@@ -207,6 +211,10 @@
msgid "About this site"
msgstr ""
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
msgid "Any"
msgstr ""
@@ -252,6 +260,10 @@
msgid "Browse by entity type"
msgstr ""
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr ""
+
msgid "Bytes"
msgstr "Bytes"
@@ -374,12 +386,6 @@
#, python-format
msgid ""
-"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
-"exists anymore in the schema."
-msgstr ""
-
-#, python-format
-msgid ""
"Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
"anymore."
msgstr ""
@@ -399,6 +405,10 @@
msgid "Click to sort on this column"
msgstr ""
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
msgid "DEBUG"
msgstr ""
@@ -424,6 +434,14 @@
msgid "Decimal_plural"
msgstr "Decimal numbers"
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr ""
+
msgid "Detected problems"
msgstr ""
@@ -846,12 +864,22 @@
msgid "URLs from which content will be imported. You can put one url per line"
msgstr ""
+msgid "Undoable actions"
+msgstr ""
+
+msgid "Undoing"
+msgstr ""
+
msgid "UniqueConstraint"
msgstr "unique constraint"
msgid "Unreachable objects"
msgstr ""
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr ""
+
msgid "Used by:"
msgstr ""
@@ -935,9 +963,6 @@
msgid "abstract base class for transitions"
msgstr ""
-msgid "action menu"
-msgstr ""
-
msgid "action(s) on this selection"
msgstr ""
@@ -1220,6 +1245,9 @@
msgid "bad value"
msgstr ""
+msgid "badly formatted url"
+msgstr ""
+
msgid "base url"
msgstr ""
@@ -1304,6 +1332,9 @@
msgid "can not resolve entity types:"
msgstr ""
+msgid "can only have one url"
+msgstr ""
+
msgid "can't be changed"
msgstr ""
@@ -1340,6 +1371,22 @@
#, python-format
msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+
+#, python-format
+msgid ""
"can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
"%(card)s"
msgstr ""
@@ -1970,9 +2017,6 @@
msgid "date"
msgstr ""
-msgid "day"
-msgstr ""
-
msgid "deactivate"
msgstr ""
@@ -2464,6 +2508,9 @@
msgid "foaf"
msgstr ""
+msgid "focus on this selection"
+msgstr ""
+
msgid "follow"
msgstr ""
@@ -2610,6 +2657,9 @@
msgid "has_text"
msgstr "has text"
+msgid "header-center"
+msgstr ""
+
msgid "header-left"
msgstr "header (left)"
@@ -2948,9 +2998,6 @@
msgid "log in"
msgstr ""
-msgid "log out first"
-msgstr ""
-
msgid "login"
msgstr ""
@@ -3052,9 +3099,6 @@
msgid "monday"
msgstr ""
-msgid "month"
-msgstr ""
-
msgid "more actions"
msgstr ""
@@ -3155,6 +3199,9 @@
msgid "new"
msgstr ""
+msgid "next page"
+msgstr ""
+
msgid "next_results"
msgstr "next results"
@@ -3355,6 +3402,9 @@
msgid "preferences"
msgstr ""
+msgid "previous page"
+msgstr ""
+
msgid "previous_results"
msgstr "previous results"
@@ -3926,6 +3976,10 @@
msgid "there is no previous page"
msgstr ""
+#, python-format
+msgid "there is no transaction #%s"
+msgstr ""
+
msgid "this action is not reversible!"
msgstr ""
@@ -4009,15 +4063,15 @@
msgid "to_state_object"
msgstr "transitions to this state"
-msgid "today"
-msgstr ""
-
msgid "todo_by"
msgstr "to do by"
msgid "toggle check boxes"
msgstr ""
+msgid "toggle filter"
+msgstr ""
+
msgid "tr_count"
msgstr "transition number"
@@ -4136,6 +4190,9 @@
msgid "unauthorized value"
msgstr ""
+msgid "undefined user"
+msgstr ""
+
msgid "undo"
msgstr ""
@@ -4163,6 +4220,9 @@
msgid "unknown vocabulary:"
msgstr ""
+msgid "unsupported protocol"
+msgstr ""
+
msgid "upassword"
msgstr "password"
@@ -4361,9 +4421,6 @@
msgid "wednesday"
msgstr ""
-msgid "week"
-msgstr ""
-
#, python-format
msgid "welcome %s !"
msgstr ""
--- a/i18n/es.po Wed Feb 22 11:57:42 2012 +0100
+++ b/i18n/es.po Tue Oct 23 15:00:53 2012 +0200
@@ -146,6 +146,10 @@
msgid "(UNEXISTANT EID)"
msgstr "(EID INEXISTENTE"
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr ""
+
msgid "**"
msgstr "0..n 0..n"
@@ -219,6 +223,10 @@
msgid "About this site"
msgstr "Información del Sistema"
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
msgid "Any"
msgstr "Cualquiera"
@@ -264,6 +272,10 @@
msgid "Browse by entity type"
msgstr "Busca por tipo de entidad"
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr ""
+
msgid "Bytes"
msgstr "Bytes"
@@ -390,14 +402,6 @@
#, python-format
msgid ""
-"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
-"exists anymore in the schema."
-msgstr ""
-"No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta "
-"relación ya no existe en el esquema."
-
-#, python-format
-msgid ""
"Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
"anymore."
msgstr ""
@@ -423,6 +427,10 @@
msgid "Click to sort on this column"
msgstr ""
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
msgid "DEBUG"
msgstr ""
@@ -448,6 +456,14 @@
msgid "Decimal_plural"
msgstr "Decimales"
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr ""
+
msgid "Detected problems"
msgstr "Problemas detectados"
@@ -875,12 +891,22 @@
"URLs desde el cual el contenido sera importado. Usted puede incluir un URL "
"por lÃnea."
+msgid "Undoable actions"
+msgstr ""
+
+msgid "Undoing"
+msgstr ""
+
msgid "UniqueConstraint"
msgstr "Restricción de Unicidad"
msgid "Unreachable objects"
msgstr "Objetos inaccesibles"
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr ""
+
msgid "Used by:"
msgstr "Utilizado por :"
@@ -985,9 +1011,6 @@
msgid "abstract base class for transitions"
msgstr "Clase de base abstracta para la transiciones"
-msgid "action menu"
-msgstr ""
-
msgid "action(s) on this selection"
msgstr "Acción(es) en esta selección"
@@ -1276,6 +1299,9 @@
msgid "bad value"
msgstr "Valor erróneo"
+msgid "badly formatted url"
+msgstr ""
+
msgid "base url"
msgstr "Url de base"
@@ -1360,6 +1386,9 @@
msgid "can not resolve entity types:"
msgstr "Imposible de interpretar los tipos de entidades:"
+msgid "can only have one url"
+msgstr ""
+
msgid "can't be changed"
msgstr "No puede ser modificado"
@@ -1396,6 +1425,22 @@
#, python-format
msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+
+#, python-format
+msgid ""
"can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
"%(card)s"
msgstr ""
@@ -2044,9 +2089,6 @@
msgid "date"
msgstr "Fecha"
-msgid "day"
-msgstr "dÃa"
-
msgid "deactivate"
msgstr "Desactivar"
@@ -2561,6 +2603,9 @@
msgid "foaf"
msgstr "Amigo de un Amigo, FOAF"
+msgid "focus on this selection"
+msgstr ""
+
msgid "follow"
msgstr "Seguir la liga"
@@ -2713,6 +2758,9 @@
msgid "has_text"
msgstr "Contiene el texto"
+msgid "header-center"
+msgstr ""
+
msgid "header-left"
msgstr "encabezado (izquierdo)"
@@ -3073,9 +3121,6 @@
msgid "log in"
msgstr "Acceder"
-msgid "log out first"
-msgstr "Desconéctese primero"
-
msgid "login"
msgstr "Usuario"
@@ -3177,9 +3222,6 @@
msgid "monday"
msgstr "Lunes"
-msgid "month"
-msgstr "mes"
-
msgid "more actions"
msgstr "Más acciones"
@@ -3282,6 +3324,9 @@
msgid "new"
msgstr "Nuevo"
+msgid "next page"
+msgstr ""
+
msgid "next_results"
msgstr "Siguientes resultados"
@@ -3483,6 +3528,9 @@
msgid "preferences"
msgstr "Preferencias"
+msgid "previous page"
+msgstr ""
+
msgid "previous_results"
msgstr "Resultados Anteriores"
@@ -4078,6 +4126,10 @@
msgid "there is no previous page"
msgstr ""
+#, python-format
+msgid "there is no transaction #%s"
+msgstr ""
+
msgid "this action is not reversible!"
msgstr "Esta acción es irreversible!."
@@ -4161,15 +4213,15 @@
msgid "to_state_object"
msgstr "Transición hacia este Estado"
-msgid "today"
-msgstr "hoy"
-
msgid "todo_by"
msgstr "Asignada a"
msgid "toggle check boxes"
msgstr "Cambiar valor"
+msgid "toggle filter"
+msgstr "esconder/mostrar el filtro"
+
msgid "tr_count"
msgstr "n° de transición"
@@ -4288,6 +4340,9 @@
msgid "unauthorized value"
msgstr "Valor no permitido"
+msgid "undefined user"
+msgstr ""
+
msgid "undo"
msgstr "Anular"
@@ -4315,6 +4370,9 @@
msgid "unknown vocabulary:"
msgstr "Vocabulario desconocido: "
+msgid "unsupported protocol"
+msgstr ""
+
msgid "upassword"
msgstr "Contraseña"
@@ -4522,9 +4580,6 @@
msgid "wednesday"
msgstr "Miércoles"
-msgid "week"
-msgstr "sem."
-
#, python-format
msgid "welcome %s !"
msgstr "¡ Bienvenido %s !"
@@ -4624,54 +4679,9 @@
"usted debe quitar la puesta en lÃnea de la relación %s que es aceptada y "
"puede ser cruzada"
-#~ msgid "(loading ...)"
-#~ msgstr "(Cargando ...)"
-
-#~ msgid "Schema of the data model"
-#~ msgstr "Esquema del modelo de datos"
-
-#~ msgid "add a CWSourceSchemaConfig"
-#~ msgstr "agregar una parte de mapeo"
-
-#~ msgid "csv entities export"
-#~ msgstr "Exportar entidades en csv"
-
-#~ msgid "follow this link if javascript is deactivated"
-#~ msgstr "Seleccione esta liga si javascript esta desactivado"
-
#~ msgid ""
-#~ "how to format date and time in the ui (\"man strftime\" for format "
-#~ "description)"
-#~ msgstr ""
-#~ "Formato de fecha y hora que se utilizará por defecto en la interfaz "
-#~ "(\"man strftime\" para mayor información del formato)"
-
-#~ msgid ""
-#~ "how to format date in the ui (\"man strftime\" for format description)"
+#~ "Can't restore relation %(rtype)s of entity %(eid)s, this relation does "
+#~ "not exists anymore in the schema."
#~ msgstr ""
-#~ "Formato de fecha que se utilizará por defecto en la interfaz (\"man "
-#~ "strftime\" para mayor información del formato)"
-
-#~ msgid ""
-#~ "how to format time in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "Formato de hora que se utilizará por defecto en la interfaz (\"man "
-#~ "strftime\" para mayor información del formato)"
-
-#~ msgid "instance schema"
-#~ msgstr "Esquema de la Instancia"
-
-#~ msgid "rdf"
-#~ msgstr "rdf"
-
-#~ msgid "rss"
-#~ msgstr "RSS"
-
-#~ msgid "siteinfo"
-#~ msgstr "información"
-
-#~ msgid "xbel"
-#~ msgstr "xbel"
-
-#~ msgid "xml"
-#~ msgstr "xml"
+#~ "No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta "
+#~ "relación ya no existe en el esquema."
--- a/i18n/fr.po Wed Feb 22 11:57:42 2012 +0100
+++ b/i18n/fr.po Tue Oct 23 15:00:53 2012 +0200
@@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2012-02-08 17:43+0100\n"
+"PO-Revision-Date: 2012-02-15 16:08+0100\n"
"Last-Translator: Logilab Team <contact@logilab.fr>\n"
"Language-Team: fr <contact@logilab.fr>\n"
"Language: \n"
@@ -147,6 +147,10 @@
msgid "(UNEXISTANT EID)"
msgstr "(EID INTROUVABLE)"
+#, python-format
+msgid "(suppressed) entity #%d"
+msgstr "entité #%d (supprimée)"
+
msgid "**"
msgstr "0..n 0..n"
@@ -219,6 +223,10 @@
msgid "About this site"
msgstr "À propos de ce site"
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr "Relation ajoutée : %(entity_from)s %(rtype)s %(entity_to)s"
+
msgid "Any"
msgstr "Tous"
@@ -264,6 +272,10 @@
msgid "Browse by entity type"
msgstr "Naviguer par type d'entité"
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr "Par %(user)s le %(dt)s [%(undo_link)s] "
+
msgid "Bytes"
msgstr "Donnée binaires"
@@ -390,14 +402,6 @@
#, python-format
msgid ""
-"Can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
-"exists anymore in the schema."
-msgstr ""
-"Ne peut restaurer la relation %(rtype)s de l'entité %(eid)s, cette relation "
-"n'existe plus dans le schéma"
-
-#, python-format
-msgid ""
"Can't restore relation %(rtype)s, %(role)s entity %(eid)s doesn't exist "
"anymore."
msgstr ""
@@ -423,6 +427,10 @@
msgid "Click to sort on this column"
msgstr "Cliquer pour trier sur cette colonne"
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr "Entité %(etype)s crée : %(entity)s"
+
msgid "DEBUG"
msgstr "DEBUG"
@@ -448,6 +456,14 @@
msgid "Decimal_plural"
msgstr "Nombres décimaux"
+#, python-format
+msgid "Delete relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr "Relation supprimée : %(entity_from)s %(rtype)s %(entity_to)s"
+
+#, python-format
+msgid "Deleted %(etype)s : %(entity)s"
+msgstr "Entité %(etype)s supprimée : %(entity)s"
+
msgid "Detected problems"
msgstr "Problèmes détectés"
@@ -875,12 +891,22 @@
"URLs depuis lesquelles le contenu sera importé. Vous pouvez mettre une URL "
"par ligne."
+msgid "Undoable actions"
+msgstr "Action annulables"
+
+msgid "Undoing"
+msgstr "Annuler"
+
msgid "UniqueConstraint"
msgstr "contrainte d'unicité"
msgid "Unreachable objects"
msgstr "Objets inaccessibles"
+#, python-format
+msgid "Updated %(etype)s : %(entity)s"
+msgstr "Entité %(etype)s mise à jour : %(entity)s"
+
msgid "Used by:"
msgstr "Utilisé par :"
@@ -985,9 +1011,6 @@
msgid "abstract base class for transitions"
msgstr "classe de base abstraite pour les transitions"
-msgid "action menu"
-msgstr "actions"
-
msgid "action(s) on this selection"
msgstr "action(s) sur cette sélection"
@@ -1227,7 +1250,7 @@
msgstr "anonyme"
msgid "anyrsetview"
-msgstr "vues \"tous les rset\""
+msgstr "vues pour tout rset"
msgid "april"
msgstr "avril"
@@ -1277,6 +1300,9 @@
msgid "bad value"
msgstr "mauvaise valeur"
+msgid "badly formatted url"
+msgstr ""
+
msgid "base url"
msgstr "url de base"
@@ -1362,6 +1388,9 @@
msgid "can not resolve entity types:"
msgstr "impossible d'interpréter les types d'entités :"
+msgid "can only have one url"
+msgstr ""
+
msgid "can't be changed"
msgstr "ne peut-être modifié"
@@ -1398,6 +1427,28 @@
#, python-format
msgid ""
+"can't restore entity %(eid)s of type %(eschema)s, target of %(rtype)s (eid "
+"%(value)s) does not exist any longer"
+msgstr ""
+"impossible de rétablir l'entité %(eid)s de type %(eschema)s, cible de la "
+"relation %(rtype)s (eid %(value)s) n'existe plus"
+
+#, python-format
+msgid ""
+"can't restore relation %(rtype)s of entity %(eid)s, this relation does not "
+"exist in the schema anymore."
+msgstr ""
+"impossible de rétablir la relation %(rtype)s sur l'entité %(eid)s, cette "
+"relation n'existe plus dans le schéma."
+
+#, python-format
+msgid "can't restore state of entity %s, it has been deleted inbetween"
+msgstr ""
+"impossible de rétablir l'état de l'entité %s, elle a été supprimée entre-"
+"temps"
+
+#, python-format
+msgid ""
"can't set inlined=True, %(stype)s %(rtype)s %(otype)s has cardinality="
"%(card)s"
msgstr ""
@@ -2050,9 +2101,6 @@
msgid "date"
msgstr "date"
-msgid "day"
-msgstr "jour"
-
msgid "deactivate"
msgstr "désactiver"
@@ -2563,6 +2611,9 @@
msgid "foaf"
msgstr "foaf"
+msgid "focus on this selection"
+msgstr "afficher cette sélection"
+
msgid "follow"
msgstr "suivre le lien"
@@ -2716,6 +2767,9 @@
msgid "has_text"
msgstr "contient le texte"
+msgid "header-center"
+msgstr "en-tête (centre)"
+
msgid "header-left"
msgstr "en-tête (gauche)"
@@ -3074,9 +3128,6 @@
msgid "log in"
msgstr "s'identifier"
-msgid "log out first"
-msgstr "déconnecter vous d'abord"
-
msgid "login"
msgstr "identifiant"
@@ -3178,9 +3229,6 @@
msgid "monday"
msgstr "lundi"
-msgid "month"
-msgstr "mois"
-
msgid "more actions"
msgstr "plus d'actions"
@@ -3283,6 +3331,9 @@
msgid "new"
msgstr "nouveau"
+msgid "next page"
+msgstr "page suivante"
+
msgid "next_results"
msgstr "résultats suivants"
@@ -3486,6 +3537,9 @@
msgid "preferences"
msgstr "préférences"
+msgid "previous page"
+msgstr "page précédente"
+
msgid "previous_results"
msgstr "résultats précédents"
@@ -4077,10 +4131,14 @@
msgstr "la valeur \"%s\" est déjà utilisée, veuillez utiliser une autre valeur"
msgid "there is no next page"
-msgstr "il n'y a pas de page suivante"
+msgstr "Il n'y a pas de page suivante"
msgid "there is no previous page"
-msgstr "il n'y a pas de page précédente"
+msgstr "Il n'y a pas de page précédente"
+
+#, python-format
+msgid "there is no transaction #%s"
+msgstr "Il n'y a pas de transaction #%s"
msgid "this action is not reversible!"
msgstr ""
@@ -4166,14 +4224,14 @@
msgid "to_state_object"
msgstr "transition vers cet état"
-msgid "today"
-msgstr "aujourd'hui"
-
msgid "todo_by"
msgstr "Ã faire par"
msgid "toggle check boxes"
-msgstr "inverser les cases à cocher"
+msgstr "afficher/masquer les cases à cocher"
+
+msgid "toggle filter"
+msgstr "afficher/masquer le filtre"
msgid "tr_count"
msgstr "n° de transition"
@@ -4293,6 +4351,9 @@
msgid "unauthorized value"
msgstr "valeur non autorisée"
+msgid "undefined user"
+msgstr "utilisateur inconnu"
+
msgid "undo"
msgstr "annuler"
@@ -4320,6 +4381,9 @@
msgid "unknown vocabulary:"
msgstr "vocabulaire inconnu : "
+msgid "unsupported protocol"
+msgstr ""
+
msgid "upassword"
msgstr "mot de passe"
@@ -4526,9 +4590,6 @@
msgid "wednesday"
msgstr "mercredi"
-msgid "week"
-msgstr "semaine"
-
#, python-format
msgid "welcome %s !"
msgstr "bienvenue %s !"
@@ -4628,27 +4689,26 @@
"vous devriez enlevé la mise en ligne de la relation %s qui est supportée et "
"peut-être croisée"
-#~ msgid "(loading ...)"
-#~ msgstr "(chargement ...)"
-
-#~ msgid "follow this link if javascript is deactivated"
-#~ msgstr "suivez ce lien si javascript est désactivé"
-
-#~ msgid ""
-#~ "how to format date and time in the ui (\"man strftime\" for format "
-#~ "description)"
-#~ msgstr ""
-#~ "comment formater la date dans l'interface (\"man strftime\" pour la "
-#~ "description du format)"
-
-#~ msgid ""
-#~ "how to format date in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "comment formater la date dans l'interface (\"man strftime\" pour la "
-#~ "description du format)"
-
-#~ msgid ""
-#~ "how to format time in the ui (\"man strftime\" for format description)"
-#~ msgstr ""
-#~ "comment formater l'heure dans l'interface (\"man strftime\" pour la "
-#~ "description du format)"
+#~ msgid "Action"
+#~ msgstr "Action"
+
+#~ msgid "day"
+#~ msgstr "jour"
+
+#~ msgid "jump to selection"
+#~ msgstr "afficher cette sélection"
+
+#~ msgid "log out first"
+#~ msgstr "déconnecter vous d'abord"
+
+#~ msgid "month"
+#~ msgstr "mois"
+
+#~ msgid "today"
+#~ msgstr "aujourd'hui"
+
+#~ msgid "undo last change"
+#~ msgstr "annuler dernier changement"
+
+#~ msgid "week"
+#~ msgstr "semaine"
--- a/md5crypt.py Wed Feb 22 11:57:42 2012 +0100
+++ b/md5crypt.py Tue Oct 23 15:00:53 2012 +0200
@@ -51,18 +51,16 @@
v = v >> 6
return ret
-def crypt(pw, salt, magic=None):
+def crypt(pw, salt):
if isinstance(pw, unicode):
pw = pw.encode('utf-8')
- if magic is None:
- magic = MAGIC
# Take care of the magic string if present
- if salt[:len(magic)] == magic:
- salt = salt[len(magic):]
+ if salt.startswith(MAGIC):
+ salt = salt[len(MAGIC):]
# salt can have up to 8 characters:
salt = salt.split('$', 1)[0]
salt = salt[:8]
- ctx = pw + magic + salt
+ ctx = pw + MAGIC + salt
final = md5(pw + salt + pw).digest()
for pl in xrange(len(pw), 0, -16):
if pl > 16:
@@ -114,4 +112,4 @@
|(int(ord(final[10])) << 8)
|(int(ord(final[5]))), 4)
passwd = passwd + to64((int(ord(final[11]))), 2)
- return salt + '$' + passwd
+ return passwd
--- a/migration.py Wed Feb 22 11:57:42 2012 +0100
+++ b/migration.py Tue Oct 23 15:00:53 2012 +0200
@@ -514,7 +514,9 @@
elif op == None:
continue
else:
- print 'unable to handle this case', oper, version, op, ver
+ print ('unable to handle %s in %s, set to `%s %s` '
+ 'but currently up to `%s %s`' %
+ (cube, source, oper, version, op, ver))
# "solve" constraint satisfaction problem
if cube not in self.cubes:
self.errors.append( ('add', cube, version, source) )
--- a/misc/migration/3.10.0_Any.py Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/3.10.0_Any.py Tue Oct 23 15:00:53 2012 +0200
@@ -34,5 +34,5 @@
for x in rql('Any X,XK WHERE X pkey XK, '
'X pkey ~= "boxes.%" OR '
'X pkey ~= "contentnavigation.%"').entities():
- x.set_attributes(pkey=u'ctxcomponents.' + x.pkey.split('.', 1)[1])
+ x.cw_set(pkey=u'ctxcomponents.' + x.pkey.split('.', 1)[1])
--- a/misc/migration/3.11.0_Any.py Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/3.11.0_Any.py Tue Oct 23 15:00:53 2012 +0200
@@ -81,5 +81,5 @@
rset = session.execute('Any V WHERE X is CWProperty, X value V, X pkey %(k)s',
{'k': pkey})
timestamp = int(rset[0][0])
- sourceentity.set_attributes(latest_retrieval=datetime.fromtimestamp(timestamp))
+ sourceentity.cw_set(latest_retrieval=datetime.fromtimestamp(timestamp))
session.execute('DELETE CWProperty X WHERE X pkey %(k)s', {'k': pkey})
--- a/misc/migration/3.12.0_Any.py Wed Feb 22 11:57:42 2012 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-if schema['TZDatetime'].eid is None:
- add_entity_type('TZDatetime')
-if schema['TZTime'].eid is None:
- add_entity_type('TZTime')
--- a/misc/migration/3.14.0_Any.py Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/3.14.0_Any.py Tue Oct 23 15:00:53 2012 +0200
@@ -9,5 +9,5 @@
expression = rqlcstr.value
mainvars = guess_rrqlexpr_mainvars(expression)
yamscstr = CONSTRAINTS[rqlcstr.type](expression, mainvars)
- rqlcstr.set_attributes(value=yamscstr.serialize())
+ rqlcstr.cw_set(value=yamscstr.serialize())
print 'updated', rqlcstr.type, rqlcstr.value.strip()
--- a/misc/migration/3.14.3_Any.py Wed Feb 22 11:57:42 2012 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-# keep the same behavior on existing instance but use the new one on new instance.
-config['https-deny-anonymous'] = True
--- a/misc/migration/3.14.4_Any.py Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/3.14.4_Any.py Tue Oct 23 15:00:53 2012 +0200
@@ -4,6 +4,8 @@
rdefdef = schema['CWSource'].rdef('name')
attrtype = y2sql.type_from_constraints(dbhelper, rdefdef.object, rdefdef.constraints).split()[0]
-sql(dbhelper.sql_change_col_type('entities', 'asource', attrtype, False))
-sql(dbhelper.sql_change_col_type('entities', 'source', attrtype, False))
-sql(dbhelper.sql_change_col_type('deleted_entities', 'source', attrtype, False))
+cursor = session.cnxset['system']
+sql('UPDATE entities SET asource = source WHERE asource is NULL')
+dbhelper.change_col_type(cursor, 'entities', 'asource', attrtype, False)
+dbhelper.change_col_type(cursor, 'entities', 'source', attrtype, False)
+dbhelper.change_col_type(cursor, 'deleted_entities', 'source', attrtype, False)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.14.7_Any.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,4 @@
+# migrate default format for TriInfo `comment_format` attribute
+sync_schema_props_perms('TrInfo')
+
+commit()
--- a/misc/migration/3.15.0_Any.py Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/3.15.0_Any.py Tue Oct 23 15:00:53 2012 +0200
@@ -4,7 +4,7 @@
config = source.dictconfig
host = config.pop('host', u'ldap')
protocol = config.pop('protocol', u'ldap')
- source.set_attributes(url=u'%s://%s' % (protocol, host))
+ source.cw_set(url=u'%s://%s' % (protocol, host))
source.update_config(skip_unknown=True, **config)
commit()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.0_common.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,7 @@
+import ConfigParser
+try:
+ undo_actions = config.cfgfile_parser.get('MAIN', 'undo-support', False)
+except ConfigParser.NoOptionError:
+ pass # this conf. file was probably already migrated
+else:
+ config.global_set_option('undo-enabled', bool(undo_actions))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.4_Any.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,11 @@
+from logilab.common.shellutils import generate_password
+from cubicweb.server.utils import crypt_password
+
+for user in rql('CWUser U WHERE U cw_source S, S name "system", U upassword P, U login L').entities():
+ salt = user.upassword.getvalue()
+ if crypt_password('', salt) == salt:
+ passwd = generate_password()
+ print 'setting random password for user %s' % user.login
+ user.set_attributes(upassword=passwd)
+
+commit()
--- a/misc/migration/bootstrapmigration_repository.py Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/bootstrapmigration_repository.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -40,6 +40,13 @@
sql('UPDATE entities SET asource=cw_name '
'FROM cw_CWSource, cw_source_relation '
'WHERE entities.eid=cw_source_relation.eid_from AND cw_source_relation.eid_to=cw_CWSource.cw_eid')
+ commit()
+
+if schema['TZDatetime'].eid is None:
+ add_entity_type('TZDatetime', auto=False)
+if schema['TZTime'].eid is None:
+ add_entity_type('TZTime', auto=False)
+
if applcubicwebversion <= (3, 14, 0) and cubicwebversion >= (3, 14, 0):
if 'require_permission' in schema and not 'localperms'in repo.config.cubes():
--- a/misc/migration/postcreate.py Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/migration/postcreate.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/misc/scripts/chpasswd.py Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/scripts/chpasswd.py Tue Oct 23 15:00:53 2012 +0200
@@ -42,7 +42,7 @@
crypted = crypt_password(pass1)
cwuser = rset.get_entity(0,0)
-cwuser.set_attributes(upassword=Binary(crypted))
+cwuser.cw_set(upassword=Binary(crypted))
commit()
print("password updated.")
--- a/misc/scripts/ldapuser2ldapfeed.py Wed Feb 22 11:57:42 2012 +0100
+++ b/misc/scripts/ldapuser2ldapfeed.py Tue Oct 23 15:00:53 2012 +0200
@@ -3,6 +3,8 @@
Once this script is run, execute c-c db-check to cleanup relation tables.
"""
import sys
+from collections import defaultdict
+from logilab.common.shellutils import generate_password
try:
source_name, = __args__
@@ -33,44 +35,65 @@
print '******************** backport entity content ***************************'
-todelete = {}
+todelete = defaultdict(list)
+extids = set()
+duplicates = []
for entity in rql('Any X WHERE X cw_source S, S eid %(s)s', {'s': source.eid}).entities():
- etype = entity.__regid__
- if not source.support_entity(etype):
- print "source doesn't support %s, delete %s" % (etype, entity.eid)
- else:
- try:
- entity.complete()
- except Exception:
- print '%s %s much probably deleted, delete it (extid %s)' % (
- etype, entity.eid, entity.cw_metainformation()['extid'])
- else:
- print 'get back', etype, entity.eid
- entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
- if not entity.creation_date:
- entity.cw_edited['creation_date'] = datetime.now()
- if not entity.modification_date:
- entity.cw_edited['modification_date'] = datetime.now()
- if not entity.upassword:
- entity.cw_edited['upassword'] = u''
- if not entity.cwuri:
- entity.cw_edited['cwuri'] = '%s/?dn=%s' % (
- source.urls[0], entity.cw_metainformation()['extid'])
- print entity.cw_edited
- system_source.add_entity(session, entity)
- sql("UPDATE entities SET source='system' "
- "WHERE eid=%(eid)s", {'eid': entity.eid})
- continue
- todelete.setdefault(etype, []).append(entity)
+ etype = entity.__regid__
+ if not source.support_entity(etype):
+ print "source doesn't support %s, delete %s" % (etype, entity.eid)
+ todelete[etype].append(entity)
+ continue
+ try:
+ entity.complete()
+ except Exception:
+ print '%s %s much probably deleted, delete it (extid %s)' % (
+ etype, entity.eid, entity.cw_metainformation()['extid'])
+ todelete[etype].append(entity)
+ continue
+ print 'get back', etype, entity.eid
+ entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
+ if not entity.creation_date:
+ entity.cw_edited['creation_date'] = datetime.now()
+ if not entity.modification_date:
+ entity.cw_edited['modification_date'] = datetime.now()
+ if not entity.upassword:
+ entity.cw_edited['upassword'] = generate_password()
+ extid = entity.cw_metainformation()['extid']
+ if not entity.cwuri:
+ entity.cw_edited['cwuri'] = '%s/?dn=%s' % (
+ source.urls[0], extid.decode('utf-8', 'ignore'))
+ print entity.cw_edited
+ if extid in extids:
+ duplicates.append(extid)
+ continue
+ extids.add(extid)
+ system_source.add_entity(session, entity)
+ sql("UPDATE entities SET source='system' "
+ "WHERE eid=%(eid)s", {'eid': entity.eid})
# only cleanup entities table, remaining stuff should be cleaned by a c-c
# db-check to be run after this script
-for entities in todelete.values():
+if duplicates:
+ print 'found %s duplicate entries' % len(duplicates)
+ from pprint import pprint
+ pprint(duplicates)
+
+print len(todelete), 'entities will be deleted'
+for etype, entities in todelete.values():
+ print 'deleting', etype, [e.login for e in entities]
system_source.delete_info_multi(session, entities, source_name)
+
source_ent = rql('CWSource S WHERE S eid %(s)s', {'s': source.eid}).get_entity(0, 0)
-source_ent.set_attributes(type=u"ldapfeed", parser=u"ldapfeed")
+source_ent.cw_set(type=u"ldapfeed", parser=u"ldapfeed")
-commit()
+if raw_input('Commit ?') in 'yY':
+ print 'committing'
+ commit()
+else:
+ rollback()
+ print 'rollbacked'
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/scripts/repair_splitbrain_ldapuser_source.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,108 @@
+"""
+CAUTION: READ THIS CAREFULLY
+
+Sometimes it happens that ldap (specifically ldapuser type) source
+yield "ghost" users. The reasons may vary (server upgrade while some
+instances are still running & syncing with the ldap source, unmanaged
+updates to the upstream ldap, etc.).
+
+This script was written and refined enough times that we are confident
+in that it does something reasonnable (at least it did for the
+target application).
+
+However you should really REALLY understand what it does before
+deciding to apply it for you. And then ADAPT it tou your needs.
+
+"""
+
+import base64
+from collections import defaultdict
+
+from cubicweb.server.session import hooks_control
+
+try:
+ source_name, = __args__
+ source = repo.sources_by_uri[source_name]
+except ValueError:
+ print('you should specify the source name as script argument (i.e. after --'
+ ' on the command line)')
+ sys.exit(1)
+except KeyError:
+ print '%s is not an active source' % source_name
+ sys.exit(1)
+
+# check source is reachable before doing anything
+if not source.get_connection().cnx:
+ print '%s is not reachable. Fix this before running this script' % source_name
+ sys.exit(1)
+
+def find_dupes():
+ # XXX this retrieves entities from a source name "ldap"
+ # you will want to adjust
+ rset = sql("SELECT eid, extid FROM entities WHERE source='%s'" % source_name)
+ extid2eids = defaultdict(list)
+ for eid, extid in rset:
+ extid2eids[extid].append(eid)
+ return dict((base64.b64decode(extid).lower(), eids)
+ for extid, eids in extid2eids.items()
+ if len(eids) > 1)
+
+def merge_dupes(dupes, docommit=False):
+ gone_eids = []
+ CWUser = schema['CWUser']
+ for extid, eids in dupes.items():
+ newest = eids.pop() # we merge everything on the newest
+ print 'merging ghosts of', extid, 'into', newest
+ # now we merge pairwise into the newest
+ for old in eids:
+ subst = {'old': old, 'new': newest}
+ print ' merging', old
+ gone_eids.append(old)
+ for rschema in CWUser.subject_relations():
+ if rschema.final or rschema == 'identity':
+ continue
+ if CWUser.rdef(rschema, 'subject').composite == 'subject':
+ # old 'composite' property is wiped ...
+ # think about email addresses, excel preferences
+ for eschema in rschema.objects():
+ rql('DELETE %s X WHERE U %s X, U eid %%(old)s' % (eschema, rschema), subst)
+ else:
+ # relink the new user to its old relations
+ rql('SET NU %s X WHERE NU eid %%(new)s, NOT NU %s X, OU %s X, OU eid %%(old)s' %
+ (rschema, rschema, rschema), subst)
+ # delete the old relations
+ rql('DELETE U %s X WHERE U eid %%(old)s' % rschema, subst)
+ # same thing ...
+ for rschema in CWUser.object_relations():
+ if rschema.final or rschema == 'identity':
+ continue
+ rql('SET X %s NU WHERE NU eid %%(new)s, NOT X %s NU, X %s OU, OU eid %%(old)s' %
+ (rschema, rschema, rschema), subst)
+ rql('DELETE X %s U WHERE U eid %%(old)s' % rschema, subst)
+ if not docommit:
+ rollback()
+ return
+ commit() # XXX flushing operations is wanted rather than really committing
+ print 'clean up entities table'
+ sql('DELETE FROM entities WHERE eid IN (%s)' % (', '.join(str(x) for x in gone_eids)))
+ commit()
+
+def main():
+ dupes = find_dupes()
+ if not dupes:
+ print 'No duplicate user'
+ return
+
+ print 'Found %s duplicate user instances' % len(dupes)
+
+ while True:
+ print 'Fix or dry-run? (f/d) ... or Ctrl-C to break out'
+ answer = raw_input('> ')
+ if answer.lower() not in 'fd':
+ continue
+ print 'Please STOP THE APPLICATION INSTANCES (service or interactive), and press Return when done.'
+ raw_input('<I swear all running instances and workers of the application are stopped>')
+ with hooks_control(session, session.HOOKS_DENY_ALL):
+ merge_dupes(dupes, docommit=answer=='f')
+
+main()
--- a/predicates.py Wed Feb 22 11:57:42 2012 +0100
+++ b/predicates.py Tue Oct 23 15:00:53 2012 +0200
@@ -352,12 +352,12 @@
"""
def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
- **kwargs):
- if not rset and not kwargs.get('entity'):
+ entity=None, **kwargs):
+ if not rset and entity is None:
return 0
score = 0
- if kwargs.get('entity'):
- score = self.score_entity(kwargs['entity'])
+ if entity is not None:
+ score = self.score_entity(entity)
elif row is None:
col = col or 0
if accept_none is None:
@@ -558,7 +558,7 @@
@objectify_predicate
def nonempty_rset(cls, req, rset=None, **kwargs):
"""Return 1 for result set containing one ore more rows."""
- if rset is not None and rset.rowcount:
+ if rset:
return 1
return 0
@@ -567,7 +567,7 @@
@objectify_predicate
def empty_rset(cls, req, rset=None, **kwargs):
"""Return 1 for result set which doesn't contain any row."""
- if rset is not None and rset.rowcount == 0:
+ if rset is not None and len(rset) == 0:
return 1
return 0
@@ -580,7 +580,7 @@
"""
if rset is None and 'entity' in kwargs:
return 1
- if rset is not None and (row is not None or rset.rowcount == 1):
+ if rset is not None and (row is not None or len(rset) == 1):
return 1
return 0
@@ -608,7 +608,7 @@
return self.operator(num, self.expected)
def __call__(self, cls, req, rset=None, **kwargs):
- return int(rset is not None and self.match_expected(rset.rowcount))
+ return int(rset is not None and self.match_expected(len(rset)))
class multi_columns_rset(multi_lines_rset):
@@ -618,8 +618,9 @@
"""
def __call__(self, cls, req, rset=None, **kwargs):
- # 'or 0' since we *must not* return None
- return rset and self.match_expected(len(rset.rows[0])) or 0
+ # 'or 0' since we *must not* return None. Also don't use rset.rows so
+ # this selector will work if rset is a simple list of list.
+ return rset and self.match_expected(len(rset[0])) or 0
class paginated_rset(Predicate):
@@ -647,7 +648,7 @@
page_size = req.property_value('navigation.page-size')
else:
page_size = int(page_size)
- if rset.rowcount <= (page_size*self.nbpages):
+ if len(rset) <= (page_size*self.nbpages):
return 0
return self.nbpages
@@ -736,12 +737,16 @@
See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
class lookup / score rules according to the input context.
- .. note:: when interface is an entity class, the score will reflect class
- proximity so the most specific object will be selected.
+ .. note::
+
+ when interface is an entity class, the score will reflect class
+ proximity so the most specific object will be selected.
- .. note:: deprecated in cubicweb >= 3.9, use either
- :class:`~cubicweb.predicates.is_instance` or
- :class:`~cubicweb.predicates.adaptable`.
+ .. note::
+
+ deprecated in cubicweb >= 3.9, use either
+ :class:`~cubicweb.predicates.is_instance` or
+ :class:`~cubicweb.predicates.adaptable`.
"""
def __init__(self, *expected_ifaces, **kwargs):
@@ -1075,9 +1080,9 @@
# don't use EntityPredicate.__call__ but this optimized implementation to
# avoid considering each entity when it's not necessary
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
- if kwargs.get('entity'):
- return self.score_entity(kwargs['entity'])
+ def __call__(self, cls, req, rset=None, row=None, col=0, entity=None, **kwargs):
+ if entity is not None:
+ return self.score_entity(entity)
if rset is None:
return 0
if row is None:
--- a/req.py Wed Feb 22 11:57:42 2012 +0100
+++ b/req.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -62,6 +62,8 @@
:attribute vreg.schema: the instance's schema
:attribute vreg.config: the instance's configuration
"""
+ is_request = True # False for repository session
+
def __init__(self, vreg):
self.vreg = vreg
try:
@@ -75,6 +77,17 @@
self.local_perm_cache = {}
self._ = unicode
+ def set_language(self, lang):
+ """install i18n configuration for `lang` translation.
+
+ Raises :exc:`KeyError` if translation doesn't exist.
+ """
+ self.lang = lang
+ gettext, pgettext = self.vreg.config.translations[lang]
+ # use _cw.__ to translate a message without registering it to the catalog
+ self._ = self.__ = gettext
+ self.pgettext = pgettext
+
def property_value(self, key):
"""return value of the property with the given key, giving priority to
user specific value if any, else using site value
@@ -204,6 +217,9 @@
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
@@ -222,7 +238,8 @@
method = 'view'
base_url = kwargs.pop('base_url', None)
if base_url is None:
- base_url = self.base_url()
+ secure = kwargs.pop('__secure__', None)
+ base_url = self.base_url(secure=secure)
if '_restpath' in kwargs:
assert method == 'view', method
path = kwargs.pop('_restpath')
@@ -415,8 +432,11 @@
raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
% {'value': value, 'format': format})
- def base_url(self):
- """return the root url of the instance"""
+ def base_url(self, secure=None):
+ """return the root url of the instance
+ """
+ if secure:
+ raise NotImplementedError()
return self.vreg.config['base-url']
# abstract methods to override according to the web front-end #############
--- a/rqlrewrite.py Wed Feb 22 11:57:42 2012 +0100
+++ b/rqlrewrite.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -77,12 +77,26 @@
mytyperel = None
possibletypes = allpossibletypes[varname]
if mytyperel is not None:
- # variable as already some types restriction. new possible types
- # can only be a subset of existing ones, so only remove no more
- # possible types
- for cst in mytyperel.get_nodes(n.Constant):
- if not cst.value in possibletypes:
- cst.parent.remove(cst)
+ if mytyperel.r_type == 'is_instance_of':
+ # turn is_instance_of relation into a is relation since we've
+ # all possible solutions and don't want to bother with
+ # potential is_instance_of incompatibility
+ mytyperel.r_type = 'is'
+ if len(possibletypes) > 1:
+ node = n.Function('IN')
+ for etype in possibletypes:
+ node.append(n.Constant(etype, 'etype'))
+ else:
+ node = n.Constant(etype, 'etype')
+ comp = mytyperel.children[1]
+ comp.replace(comp.children[0], node)
+ else:
+ # variable has already some strict types restriction. new
+ # possible types can only be a subset of existing ones, so only
+ # remove no more possible types
+ for cst in mytyperel.get_nodes(n.Constant):
+ if not cst.value in possibletypes:
+ cst.parent.remove(cst)
else:
# we have to add types restriction
if stinfo.get('scope') is not None:
@@ -228,39 +242,44 @@
if not r in sti['rhsrelations'])
else:
vi['rhs_rels'] = vi['lhs_rels'] = {}
- parent = None
+ previous = None
inserted = False
for rqlexpr in rqlexprs:
self.current_expr = rqlexpr
if varexistsmap is None:
try:
- new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, parent)
+ new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, previous)
except Unsupported:
continue
inserted = True
if new is not None and self._insert_scope is None:
self.exists_snippet[rqlexpr] = new
- parent = parent or new
+ previous = previous or new
else:
# called to reintroduce snippet due to ambiguity creation,
# so skip snippets which are not introducing this ambiguity
exists = varexistsmap[varmap]
- if self.exists_snippet[rqlexpr] is exists:
+ if self.exists_snippet.get(rqlexpr) is exists:
self.insert_snippet(varmap, rqlexpr.snippet_rqlst, exists)
if varexistsmap is None and not inserted:
# no rql expression found matching rql solutions. User has no access right
raise Unauthorized() # XXX may also be because of bad constraints in schema definition
- def insert_snippet(self, varmap, snippetrqlst, parent=None):
+ def insert_snippet(self, varmap, snippetrqlst, previous=None):
new = snippetrqlst.where.accept(self)
existing = self.existingvars
self.existingvars = None
try:
- return self._insert_snippet(varmap, parent, new)
+ return self._insert_snippet(varmap, previous, new)
finally:
self.existingvars = existing
- def _insert_snippet(self, varmap, parent, new):
+ def _insert_snippet(self, varmap, previous, new):
+ """insert `new` snippet into the syntax tree, which have been rewritten
+ using `varmap`. In cases where an action is protected by several rql
+ expresssion, `previous` will be the first rql expression which has been
+ inserted, and so should be ORed with the following expressions.
+ """
if new is not None:
if self._insert_scope is None:
insert_scope = None
@@ -274,28 +293,28 @@
insert_scope = self._insert_scope
if self._insert_scope is None and any(vi.get('stinfo', {}).get('optrelations')
for vi in self.varinfos):
- assert parent is None
- self._insert_scope = self.snippet_subquery(varmap, new)
+ assert previous is None
+ self._insert_scope, new = self.snippet_subquery(varmap, new)
self.insert_pending()
#self._insert_scope = None
- return
+ return new
if not isinstance(new, (n.Exists, n.Not)):
new = n.Exists(new)
- if parent is None:
+ if previous is None:
insert_scope.add_restriction(new)
else:
- grandpa = parent.parent
- or_ = n.Or(parent, new)
- grandpa.replace(parent, or_)
+ grandpa = previous.parent
+ or_ = n.Or(previous, new)
+ grandpa.replace(previous, or_)
if not self.removing_ambiguity:
try:
self.compute_solutions()
except Unsupported:
# some solutions have been lost, can't apply this rql expr
- if parent is None:
+ if previous is None:
self.current_statement().remove_node(new, undefine=True)
else:
- grandpa.replace(or_, parent)
+ grandpa.replace(or_, previous)
self._cleanup_inserted(new)
raise
else:
@@ -419,7 +438,7 @@
# some solutions have been lost, can't apply this rql expr
self.select.remove_subquery(self.select.with_[-1])
raise
- return subselect
+ return subselect, snippetrqlst
def remove_ambiguities(self, snippets, newsolutions):
# the snippet has introduced some ambiguities, we have to resolve them
@@ -476,11 +495,17 @@
def _cleanup_inserted(self, node):
# cleanup inserted variable references
+ removed = set()
for vref in node.iget_nodes(n.VariableRef):
vref.unregister_reference()
if not vref.variable.stinfo['references']:
# no more references, undefine the variable
del self.select.defined_vars[vref.name]
+ removed.add(vref.name)
+ for key, newvar in self.rewritten.items(): # I mean items we alter it
+ if newvar in removed:
+ del self.rewritten[key]
+
def _may_be_shared_with(self, sniprel, target):
"""if the snippet relation can be skipped to use a relation from the
--- a/schema.py Wed Feb 22 11:57:42 2012 +0100
+++ b/schema.py Tue Oct 23 15:00:53 2012 +0200
@@ -261,30 +261,34 @@
return self.has_local_role(action) or self.has_perm(req, action)
PermissionMixIn.may_have_permission = may_have_permission
-def has_perm(self, session, action, **kwargs):
+def has_perm(self, _cw, action, **kwargs):
"""return true if the action is granted globaly or localy"""
try:
- self.check_perm(session, action, **kwargs)
+ self.check_perm(_cw, action, **kwargs)
return True
except Unauthorized:
return False
PermissionMixIn.has_perm = has_perm
-def check_perm(self, session, action, **kwargs):
- # NB: session may be a server session or a request object check user is
- # in an allowed group, if so that's enough internal sessions should
- # always stop there
+def check_perm(self, _cw, action, **kwargs):
+ # NB: _cw may be a server transaction or a request object.
+ #
+ # check user is in an allowed group, if so that's enough internal
+ # transactions should always stop there
groups = self.get_groups(action)
- if session.user.matching_groups(groups):
+ if _cw.user.matching_groups(groups):
return
# if 'owners' in allowed groups, check if the user actually owns this
# object, if so that's enough
+ #
+ # NB: give _cw to user.owns since user is not be bound to a transaction on
+ # the repository side
if 'owners' in groups and (
kwargs.get('creating')
- or ('eid' in kwargs and session.user.owns(kwargs['eid']))):
+ or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))):
return
# else if there is some rql expressions, check them
- if any(rqlexpr.check(session, **kwargs)
+ if any(rqlexpr.check(_cw, **kwargs)
for rqlexpr in self.get_rqlexprs(action)):
return
raise Unauthorized(action, str(self))
@@ -467,45 +471,45 @@
return True
return False
- def has_perm(self, session, action, **kwargs):
+ def has_perm(self, _cw, action, **kwargs):
"""return true if the action is granted globaly or localy"""
if self.final:
assert not ('fromeid' in kwargs or 'toeid' in kwargs), kwargs
assert action in ('read', 'update')
if 'eid' in kwargs:
- subjtype = session.describe(kwargs['eid'])[0]
+ subjtype = _cw.describe(kwargs['eid'])[0]
else:
subjtype = objtype = None
else:
assert not 'eid' in kwargs, kwargs
assert action in ('read', 'add', 'delete')
if 'fromeid' in kwargs:
- subjtype = session.describe(kwargs['fromeid'])[0]
+ subjtype = _cw.describe(kwargs['fromeid'])[0]
elif 'frometype' in kwargs:
subjtype = kwargs.pop('frometype')
else:
subjtype = None
if 'toeid' in kwargs:
- objtype = session.describe(kwargs['toeid'])[0]
+ objtype = _cw.describe(kwargs['toeid'])[0]
elif 'toetype' in kwargs:
objtype = kwargs.pop('toetype')
else:
objtype = None
if objtype and subjtype:
- return self.rdef(subjtype, objtype).has_perm(session, action, **kwargs)
+ return self.rdef(subjtype, objtype).has_perm(_cw, action, **kwargs)
elif subjtype:
for tschema in self.targets(subjtype, 'subject'):
rdef = self.rdef(subjtype, tschema)
- if not rdef.has_perm(session, action, **kwargs):
+ if not rdef.has_perm(_cw, action, **kwargs):
return False
elif objtype:
for tschema in self.targets(objtype, 'object'):
rdef = self.rdef(tschema, objtype)
- if not rdef.has_perm(session, action, **kwargs):
+ if not rdef.has_perm(_cw, action, **kwargs):
return False
else:
for rdef in self.rdefs.itervalues():
- if not rdef.has_perm(session, action, **kwargs):
+ if not rdef.has_perm(_cw, action, **kwargs):
return False
return True
@@ -754,17 +758,17 @@
return rql, found, keyarg
return rqlst.as_string(), None, None
- def _check(self, session, **kwargs):
+ def _check(self, _cw, **kwargs):
"""return True if the rql expression is matching the given relation
between fromeid and toeid
- session may actually be a request as well
+ _cw may be a request or a server side transaction
"""
creating = kwargs.get('creating')
if not creating and self.eid is not None:
key = (self.eid, tuple(sorted(kwargs.iteritems())))
try:
- return session.local_perm_cache[key]
+ return _cw.local_perm_cache[key]
except KeyError:
pass
rql, has_perm_defs, keyarg = self.transform_has_permission()
@@ -772,50 +776,50 @@
if creating and 'X' in self.rqlst.defined_vars:
return True
if keyarg is None:
- kwargs.setdefault('u', session.user.eid)
+ kwargs.setdefault('u', _cw.user.eid)
try:
- rset = session.execute(rql, kwargs, build_descr=True)
+ 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:
- session.local_perm_cache[key] = False
+ _cw.local_perm_cache[key] = False
return False
except TypeResolverException, ex:
# some expression may not be resolvable with current kwargs
# (type conflict)
self.warning('%s: %s', rql, str(ex))
if self.eid is not None:
- session.local_perm_cache[key] = False
+ _cw.local_perm_cache[key] = False
return False
except Unauthorized, ex:
self.debug('unauthorized %s: %s', rql, str(ex))
if self.eid is not None:
- session.local_perm_cache[key] = False
+ _cw.local_perm_cache[key] = False
return False
else:
- rset = session.eid_rset(kwargs[keyarg])
+ rset = _cw.eid_rset(kwargs[keyarg])
# if no special has_*_permission relation in the rql expression, just
# check the result set contains something
if has_perm_defs is None:
if rset:
if self.eid is not None:
- session.local_perm_cache[key] = True
+ _cw.local_perm_cache[key] = True
return True
elif rset:
# check every special has_*_permission relation is satisfied
- get_eschema = session.vreg.schema.eschema
+ get_eschema = _cw.vreg.schema.eschema
try:
for eaction, col in has_perm_defs:
for i in xrange(len(rset)):
eschema = get_eschema(rset.description[i][col])
- eschema.check_perm(session, eaction, eid=rset[i][col])
+ eschema.check_perm(_cw, eaction, eid=rset[i][col])
if self.eid is not None:
- session.local_perm_cache[key] = True
+ _cw.local_perm_cache[key] = True
return True
except Unauthorized:
pass
if self.eid is not None:
- session.local_perm_cache[key] = False
+ _cw.local_perm_cache[key] = False
return False
@property
@@ -843,15 +847,15 @@
rql += ', U eid %(u)s'
return rql
- def check(self, session, eid=None, creating=False, **kwargs):
+ def check(self, _cw, eid=None, creating=False, **kwargs):
if 'X' in self.rqlst.defined_vars:
if eid is None:
if creating:
- return self._check(session, creating=True, **kwargs)
+ return self._check(_cw, creating=True, **kwargs)
return False
assert creating == False
- return self._check(session, x=eid, **kwargs)
- return self._check(session, **kwargs)
+ return self._check(_cw, x=eid, **kwargs)
+ return self._check(_cw, **kwargs)
def vargraph(rqlst):
@@ -904,7 +908,7 @@
rql += ', U eid %(u)s'
return rql
- def check(self, session, fromeid=None, toeid=None):
+ def check(self, _cw, fromeid=None, toeid=None):
kwargs = {}
if 'S' in self.rqlst.defined_vars:
if fromeid is None:
@@ -914,7 +918,7 @@
if toeid is None:
return False
kwargs['o'] = toeid
- return self._check(session, **kwargs)
+ return self._check(_cw, **kwargs)
# in yams, default 'update' perm for attributes granted to managers and owners.
@@ -1024,7 +1028,7 @@
'expression': self.expression}
raise ValidationError(maineid, {qname: msg})
- def exec_query(self, session, eidfrom, eidto):
+ def exec_query(self, _cw, eidfrom, eidto):
if eidto is None:
# checking constraint for an attribute relation
expression = 'S eid %(s)s, ' + self.expression
@@ -1034,11 +1038,11 @@
args = {'s': eidfrom, 'o': eidto}
if 'U' in self.rqlst.defined_vars:
expression = 'U eid %(u)s, ' + expression
- args['u'] = session.user.eid
+ args['u'] = _cw.user.eid
rql = 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)), expression)
if self.distinct_query:
rql = 'DISTINCT ' + rql
- return session.execute(rql, args, build_descr=False)
+ return _cw.execute(rql, args, build_descr=False)
class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint):
@@ -1061,7 +1065,7 @@
"""
# XXX turns mainvars into a required argument in __init__
distinct_query = True
-
+
def match_condition(self, session, eidfrom, eidto):
return len(self.exec_query(session, eidfrom, eidto)) <= 1
--- a/schemas/workflow.py Wed Feb 22 11:57:42 2012 +0100
+++ b/schemas/workflow.py Tue Oct 23 15:00:53 2012 +0200
@@ -185,7 +185,7 @@
# make by_transition optional because we want to allow managers to set
# entity into an arbitrary state without having to respect wf transition
by_transition = SubjectRelation('BaseTransition', cardinality='?*')
- comment = RichString(fulltextindexed=True)
+ comment = RichString(fulltextindexed=True, default_format='text/plain')
tr_count = Int(description='autocomputed attribute used to ensure transition coherency')
# get actor and date time using owned_by and creation_date
--- a/server/__init__.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/__init__.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -31,10 +31,12 @@
from logilab.common.modutils import LazyObject
from logilab.common.textutils import splitstrip
+from logilab.common.registry import yes
from yams import BASE_GROUPS
from cubicweb import CW_SOFTWARE_ROOT
+from cubicweb.appobject import AppObject
class ShuttingDown(BaseException):
"""raised when trying to access some resources while the repository is
@@ -42,7 +44,26 @@
catch it.
"""
-# server-side debugging #########################################################
+# server-side services #########################################################
+
+class Service(AppObject):
+ """Base class for services.
+
+ A service is a selectable object that performs an action server-side.
+ Use :class:`cubicweb.dbapi.Connection.call_service` to call them from
+ the web-side.
+
+ When inheriting this class, do not forget to define at least the __regid__
+ attribute (and probably __select__ too).
+ """
+ __registry__ = 'services'
+ __select__ = yes()
+
+ def call(self, **kwargs):
+ raise NotImplementedError
+
+
+# server-side debugging ########################################################
# server debugging flags. They may be combined using binary operators.
@@ -203,10 +224,6 @@
config._cubes = None # avoid assertion error
repo, cnx = in_memory_repo_cnx(config, login, password=pwd)
repo.system_source.eid = ssource.eid # redo this manually
- # trigger vreg initialisation of entity classes
- config.cubicweb_appobject_path = set(('entities',))
- config.cube_appobject_path = set(('entities',))
- repo.vreg.set_schema(repo.schema)
assert len(repo.sources) == 1, repo.sources
handler = config.migration_handler(schema, interactive=False,
cnx=cnx, repo=repo)
@@ -230,15 +247,13 @@
def initialize_schema(config, schema, mhandler, event='create'):
from cubicweb.server.schemaserial import serialize_schema
- from cubicweb.server.session import hooks_control
session = mhandler.session
cubes = config.cubes()
# deactivate every hooks but those responsible to set metadata
# so, NO INTEGRITY CHECKS are done, to have quicker db creation.
# Active integrity is kept else we may pb such as two default
# workflows for one entity type.
- with hooks_control(session, session.HOOKS_DENY_ALL, 'metadata',
- 'activeintegrity'):
+ with session.deny_all_hooks_but('metadata', 'activeintegrity'):
# execute cubicweb's pre<event> script
mhandler.cmd_exec_event_script('pre%s' % event)
# execute cubes pre<event> script if any
@@ -275,4 +290,5 @@
'ldapfeed': LazyObject('cubicweb.server.sources.ldapfeed', 'LDAPFeedSource'),
'ldapuser': LazyObject('cubicweb.server.sources.ldapuser', 'LDAPUserSource'),
'pyrorql': LazyObject('cubicweb.server.sources.pyrorql', 'PyroRQLSource'),
+ 'zmqrql': LazyObject('cubicweb.server.sources.zmqrql', 'ZMQRQLSource'),
}
--- a/server/checkintegrity.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/checkintegrity.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -32,7 +32,6 @@
from cubicweb.schema import PURE_VIRTUAL_RTYPES, VIRTUAL_RTYPES
from cubicweb.server.sqlutils import SQL_PREFIX
-from cubicweb.server.session import security_enabled
def notify_fixed(fix):
if fix:
@@ -134,8 +133,12 @@
# attribute to their current value
source = repo.system_source
for eschema in etypes:
- rset = session.execute('Any X WHERE X is %s' % eschema)
- source.fti_index_entities(session, rset.entities())
+ etype_class = session.vreg['etypes'].etype_class(str(eschema))
+ for fti_rql in etype_class.cw_fti_index_rql_queries(session):
+ rset = session.execute(fti_rql)
+ source.fti_index_entities(session, rset.entities())
+ # clear entity cache to avoid high memory consumption on big tables
+ session.drop_entity_cache()
if withpb:
pb.update()
@@ -313,7 +316,7 @@
print 'Checking mandatory relations'
msg = '%s #%s is missing mandatory %s relation %s (autofix will delete the entity)'
for rschema in schema.relations():
- if rschema.final or rschema.type in PURE_VIRTUAL_RTYPES:
+ if rschema.final or rschema in PURE_VIRTUAL_RTYPES or rschema in ('is', 'is_instance_of'):
continue
smandatory = set()
omandatory = set()
@@ -390,7 +393,7 @@
# yo, launch checks
if checks:
eids_cache = {}
- with security_enabled(session, read=False, write=False): # ensure no read security
+ with session.security_enabled(read=False, write=False): # ensure no read security
for check in checks:
check_func = globals()['check_%s' % check]
check_func(repo.schema, session, eids_cache, fix=fix)
--- a/server/cwzmq.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/cwzmq.py Tue Oct 23 15:00:53 2012 +0200
@@ -18,12 +18,16 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
from threading import Thread
+import cPickle
+import traceback
+
import zmq
from zmq.eventloop import ioloop
import zmq.eventloop.zmqstream
from logging import getLogger
from cubicweb import set_log_methods
+from cubicweb.server.server import QuitEvent
ctx = zmq.Context()
@@ -105,5 +109,132 @@
self.ioloop.add_callback(lambda: self.stream.setsockopt(zmq.SUBSCRIBE, topic))
+class ZMQRepositoryServer(object):
+
+ def __init__(self, repository):
+ """make the repository available as a PyRO object"""
+ self.address = None
+ self.repo = repository
+ self.socket = None
+ self.stream = None
+ self.loop = ioloop.IOLoop()
+
+ # event queue
+ self.events = []
+
+ def connect(self, address):
+ self.address = address
+
+ def run(self):
+ """enter the service loop"""
+ # start repository looping tasks
+ self.socket = ctx.socket(zmq.REP)
+ self.stream = zmq.eventloop.zmqstream.ZMQStream(self.socket, io_loop=self.loop)
+ self.stream.bind(self.address)
+ self.info('ZMQ server bound on: %s', self.address)
+
+ self.stream.on_recv(self.process_cmds)
+
+ try:
+ self.loop.start()
+ except zmq.ZMQError:
+ self.warning('ZMQ event loop killed')
+ self.quit()
+
+ def trigger_events(self):
+ """trigger ready events"""
+ for event in self.events[:]:
+ if event.is_ready():
+ self.info('starting event %s', event)
+ event.fire(self)
+ try:
+ event.update()
+ except Finished:
+ self.events.remove(event)
+
+ def process_cmd(self, cmd):
+ """Delegate the given command to the repository.
+
+ ``cmd`` is a list of (method_name, args, kwargs)
+ where ``args`` is a list of positional arguments
+ and ``kwargs`` is a dictionnary of named arguments.
+
+ >>> rset = delegate_to_repo(["execute", [sessionid], {'rql': rql}])
+
+ :note1: ``kwargs`` may be ommited
+
+ >>> rset = delegate_to_repo(["execute", [sessionid, rql]])
+
+ :note2: both ``args`` and ``kwargs`` may be omitted
+
+ >>> schema = delegate_to_repo(["get_schema"])
+ >>> schema = delegate_to_repo("get_schema") # also allowed
+
+ """
+ cmd = cPickle.loads(cmd)
+ if not cmd:
+ raise AttributeError('function name required')
+ if isinstance(cmd, basestring):
+ cmd = [cmd]
+ if len(cmd) < 2:
+ cmd.append(())
+ if len(cmd) < 3:
+ cmd.append({})
+ cmd = list(cmd) + [(), {}]
+ funcname, args, kwargs = cmd[:3]
+ result = getattr(self.repo, funcname)(*args, **kwargs)
+ return result
+
+ def process_cmds(self, cmds):
+ """Callback intended to be used with ``on_recv``.
+
+ Call ``delegate_to_repo`` on each command and send a pickled of
+ each result recursively.
+
+ Any exception are catched, pickled and sent.
+ """
+ try:
+ for cmd in cmds:
+ result = self.process_cmd(cmd)
+ self.send_data(result)
+ except Exception, exc:
+ traceback.print_exc()
+ self.send_data(exc)
+
+ def send_data(self, data):
+ self.socket.send_pyobj(data)
+
+ def quit(self, shutdown_repo=False):
+ """stop the server"""
+ self.info('Quitting ZMQ server')
+ try:
+ self.loop.add_callback(self.loop.stop)
+ self.stream.on_recv(None)
+ self.stream.close()
+ except Exception, e:
+ print e
+ pass
+ if shutdown_repo and not self.repo.shutting_down:
+ event = QuitEvent()
+ event.fire(self)
+
+ # server utilitities ######################################################
+
+ def install_sig_handlers(self):
+ """install signal handlers"""
+ import signal
+ self.info('installing signal handlers')
+ signal.signal(signal.SIGINT, lambda x, y, s=self: s.quit(shutdown_repo=True))
+ signal.signal(signal.SIGTERM, lambda x, y, s=self: s.quit(shutdown_repo=True))
+
+
+ # these are overridden by set_log_methods below
+ # only defining here to prevent pylint from complaining
+ @classmethod
+ def info(cls, msg, *a, **kw):
+ pass
+
+
set_log_methods(Publisher, getLogger('cubicweb.zmq.pub'))
set_log_methods(Subscriber, getLogger('cubicweb.zmq.sub'))
+set_log_methods(ZMQRepositoryServer, getLogger('cubicweb.zmq.repo'))
--- a/server/edition.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/edition.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -61,6 +61,8 @@
# attributes, else we may accidentaly skip a desired security check
if attr not in self:
self.skip_security.add(attr)
+ # mark attribute as needing purge by the client
+ self.entity._cw_dont_cache_attribute(attr)
self.edited_attribute(attr, value)
def __delitem__(self, attr):
@@ -141,8 +143,7 @@
for rtype in self]
try:
entity.e_schema.check(dict_protocol_catcher(entity),
- creation=creation, _=entity._cw._,
- relations=relations)
+ creation=creation, relations=relations)
except ValidationError, ex:
ex.entity = self.entity
raise
--- a/server/hook.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/hook.py Tue Oct 23 15:00:53 2012 +0200
@@ -152,7 +152,7 @@
On those events, the entity has no `cw_edited` dictionary.
-.. note:: `self.entity.set_attributes(age=42)` will set the `age` attribute to
+.. note:: `self.entity.cw_set(age=42)` will set the `age` attribute to
42. But to do so, it will generate a rql query that will have to be processed,
hence may trigger some hooks, etc. This could lead to infinitely looping hooks.
@@ -174,14 +174,17 @@
Non data events
~~~~~~~~~~~~~~~
-Hooks called on server start/maintenance/stop event (eg `server_startup`,
-`server_maintenance`, `server_shutdown`) have a `repo` attribute, but *their
-`_cw` attribute is None*. The `server_startup` is called on regular startup,
-while `server_maintenance` is called on cubicweb-ctl upgrade or shell
-commands. `server_shutdown` is called anyway.
+Hooks called on server start/maintenance/stop event (e.g.
+`server_startup`, `server_maintenance`, `before_server_shutdown`,
+`server_shutdown`) have a `repo` attribute, but *their `_cw` attribute
+is None*. The `server_startup` is called on regular startup, while
+`server_maintenance` is called on cubicweb-ctl upgrade or shell
+commands. `server_shutdown` is called anyway but connections to the
+native source is impossible; `before_server_shutdown` handles that.
-Hooks called on backup/restore event (eg 'server_backup', 'server_restore') have
-a `repo` and a `timestamp` attributes, but *their `_cw` attribute is None*.
+Hooks called on backup/restore event (eg `server_backup`,
+`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.
@@ -194,14 +197,12 @@
~~~~~~~~~~~~~
It is sometimes convenient to explicitly enable or disable some hooks. For
-instance if you want to disable some integrity checking hook. This can be
+instance if you want to disable some integrity checking hook. This can be
controlled more finely through the `category` class attribute, which is a string
giving a category name. One can then uses the
-:class:`~cubicweb.server.session.hooks_control` context manager to explicitly
-enable or disable some categories.
-
-.. autoclass:: cubicweb.server.session.hooks_control
-
+:meth:`~cubicweb.server.session.Session.deny_all_hooks_but` and
+:meth:`~cubicweb.server.session.Session.allow_all_hooks_but` context managers to
+explicitly enable or disable some categories.
The existing categories are:
@@ -227,14 +228,12 @@
* ``bookmark``, bookmark entities handling hooks
-Nothing precludes one to invent new categories and use the
-:class:`~cubicweb.server.session.hooks_control` context manager to
-filter them in or out. Note that ending the transaction with commit()
-or rollback() will restore the hooks.
+Nothing precludes one to invent new categories and use existing mechanisms to
+filter them in or out.
-Hooks specific predicate
-~~~~~~~~~~~~~~~~~~~~~~~
+Hooks specific predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~
.. autoclass:: cubicweb.server.hook.match_rtype
.. autoclass:: cubicweb.server.hook.match_rtype_sets
@@ -265,7 +264,6 @@
from cubicweb.cwvreg import CWRegistry, CWRegistryStore
from cubicweb.predicates import ExpectedValuePredicate, is_instance
from cubicweb.appobject import AppObject
-from cubicweb.server.session import security_enabled
ENTITIES_HOOKS = set(('before_add_entity', 'after_add_entity',
'before_update_entity', 'after_update_entity',
@@ -273,7 +271,8 @@
RELATIONS_HOOKS = set(('before_add_relation', 'after_add_relation' ,
'before_delete_relation','after_delete_relation'))
SYSTEM_HOOKS = set(('server_backup', 'server_restore',
- 'server_startup', 'server_maintenance', 'server_shutdown',
+ 'server_startup', 'server_maintenance',
+ 'server_shutdown', 'before_server_shutdown',
'session_open', 'session_close'))
ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
@@ -322,13 +321,13 @@
pruned = self.get_pruned_hooks(session, event,
entities, eids_from_to, kwargs)
# by default, hooks are executed with security turned off
- with security_enabled(session, read=False):
+ with session.security_enabled(read=False):
for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs):
hooks = sorted(self.filtered_possible_objects(pruned, session, **_kwargs),
key=lambda x: x.order)
- with security_enabled(session, write=False):
+ with session.security_enabled(write=False):
for hook in hooks:
- hook()
+ hook()
def get_pruned_hooks(self, session, event, entities, eids_from_to, kwargs):
"""return a set of hooks that should not be considered by filtered_possible objects
@@ -469,16 +468,18 @@
argument. The goal of this predicate is that it keeps reference to original sets,
so modification to thoses sets are considered by the predicate. For instance
- MYSET = set()
+ .. sourcecode:: python
+
+ MYSET = set()
- class Hook1(Hook):
- __regid__ = 'hook1'
- __select__ = Hook.__select__ & match_rtype_sets(MYSET)
- ...
+ class Hook1(Hook):
+ __regid__ = 'hook1'
+ __select__ = Hook.__select__ & match_rtype_sets(MYSET)
+ ...
- class Hook2(Hook):
- __regid__ = 'hook2'
- __select__ = Hook.__select__ & match_rtype_sets(MYSET)
+ class Hook2(Hook):
+ __regid__ = 'hook2'
+ __select__ = Hook.__select__ & match_rtype_sets(MYSET)
Client code can now change `MYSET`, this will changes the selection criteria
of :class:`Hook1` and :class:`Hook1`.
--- a/server/ldaputils.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/ldaputils.py Tue Oct 23 15:00:53 2012 +0200
@@ -37,7 +37,7 @@
from ldap.filter import filter_format
from ldapurl import LDAPUrl
-from cubicweb import ValidationError, AuthenticationError
+from cubicweb import ValidationError, AuthenticationError, Binary
from cubicweb.server.sources import ConnectionWrapper
_ = unicode
@@ -125,7 +125,7 @@
}),
('user-attrs-map',
{'type' : 'named',
- 'default': {'uid': 'login', 'gecos': 'email'},
+ 'default': {'uid': 'login', 'gecos': 'email', 'userPassword': 'upassword'},
'help': 'map from ldap user attributes to cubicweb attributes (with Active Directory, you want to use sAMAccountName:login,mail:email,givenName:firstname,sn:surname)',
'group': 'ldap-source', 'level': 1,
}),
@@ -203,7 +203,7 @@
try:
user = self._search(session, self.user_base_dn,
self.user_base_scope, searchstr)[0]
- except IndexError:
+ except (IndexError, ldap.SERVER_DOWN):
# no such user
raise AuthenticationError()
# check password by establishing a (unused) connection
@@ -216,7 +216,7 @@
except Exception:
self.error('while trying to authenticate %s', user, exc_info=True)
raise AuthenticationError()
- eid = self.repo.extid2eid(self, user['dn'], 'CWUser', session)
+ eid = self.repo.extid2eid(self, user['dn'], 'CWUser', session, {})
if eid < 0:
# user has been moved away from this source
raise AuthenticationError()
@@ -225,11 +225,12 @@
def object_exists_in_ldap(self, dn):
cnx = self.get_connection().cnx #session.cnxset.connection(self.uri).cnx
if cnx is None:
- return True # ldap unreachable, suppose it exists
+ self.warning('Could not establish connexion with LDAP server, assuming dn %s exists', dn)
+ return True # ldap unreachable, let's not touch it
try:
cnx.search_s(dn, self.user_base_scope)
except ldap.PARTIAL_RESULTS:
- pass
+ self.warning('PARTIAL RESULTS for dn %s', dn)
except ldap.NO_SUCH_OBJECT:
return False
return True
@@ -249,10 +250,11 @@
except ldap.LDAPError: # Invalid protocol version, fall back safely
conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION2)
# Deny auto-chasing of referrals to be safe, we handle them instead
- #try:
- # connection.set_option(ldap.OPT_REFERRALS, 0)
- #except ldap.LDAPError: # Cannot set referrals, so do nothing
- # pass
+ # Required for AD
+ try:
+ conn.set_option(ldap.OPT_REFERRALS, 0)
+ except ldap.LDAPError: # Cannot set referrals, so do nothing
+ pass
#conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
#conn.timeout = op_timeout
# Now bind with the credentials given. Let exceptions propagate out.
@@ -344,14 +346,13 @@
"""Turn an ldap received item into a proper dict."""
itemdict = {'dn': dn}
for key, value in iterator:
- if not isinstance(value, str):
- try:
- for i in range(len(value)):
- value[i] = unicode(value[i], 'utf8')
- except Exception:
- pass
- if isinstance(value, list) and len(value) == 1:
- itemdict[key] = value = value[0]
+ if self.user_attrs.get(key) == 'upassword': # XXx better password detection
+ itemdict[key] = Binary(value[0].encode('utf-8'))
+ else:
+ for i, val in enumerate(value):
+ value[i] = unicode(val, 'utf-8', 'replace')
+ if isinstance(value, list) and len(value) == 1:
+ itemdict[key] = value = value[0]
return itemdict
def _process_no_such_object(self, session, dn):
--- a/server/migractions.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/migractions.py Tue Oct 23 15:00:53 2012 +0200
@@ -1039,17 +1039,15 @@
gmap = self.group_mapping()
cmap = self.cstrtype_mapping()
done = set()
- for rdef in rschema.rdefs.itervalues():
- if not (reposchema.has_entity(rdef.subject)
- and reposchema.has_entity(rdef.object)):
+ for subj, obj in rschema.rdefs:
+ if not (reposchema.has_entity(subj)
+ and reposchema.has_entity(obj)):
continue
# symmetric relations appears twice
- if (rdef.subject, rdef.object) in done:
+ if (subj, obj) in done:
continue
- done.add( (rdef.subject, rdef.object) )
- self._set_rdef_eid(rdef)
- ss.execschemarql(execute, rdef,
- ss.rdef2rql(rdef, cmap, gmap))
+ done.add( (subj, obj) )
+ self.cmd_add_relation_definition(subj, rtype, obj)
if rtype in META_RTYPES:
# if the relation is in META_RTYPES, ensure we're adding it for
# all entity types *in the persistent schema*, not only those in
@@ -1077,7 +1075,7 @@
if commit:
self.commit()
- def cmd_rename_relation(self, oldname, newname, commit=True):
+ def cmd_rename_relation_type(self, oldname, newname, commit=True):
"""rename an existing relation
`oldname` is a string giving the name of the existing relation
@@ -1099,9 +1097,8 @@
print 'warning: relation %s %s %s is already known, skip addition' % (
subjtype, rtype, objtype)
return
- execute = self._cw.execute
rdef = self._get_rdef(rschema, subjtype, objtype)
- ss.execschemarql(execute, rdef,
+ ss.execschemarql(self._cw.execute, rdef,
ss.rdef2rql(rdef, self.cstrtype_mapping(),
self.group_mapping()))
if commit:
@@ -1115,7 +1112,7 @@
schemaobj = getattr(rdef, attr)
if getattr(schemaobj, 'eid', None) is None:
schemaobj.eid = self.repo.schema[schemaobj].eid
- assert schemaobj.eid is not None
+ assert schemaobj.eid is not None, schemaobj
return rdef
def cmd_drop_relation_definition(self, subjtype, rtype, objtype, commit=True):
@@ -1324,7 +1321,7 @@
except Exception:
self.cmd_create_entity('CWProperty', pkey=unicode(pkey), value=value)
else:
- prop.set_attributes(value=value)
+ prop.cw_set(value=value)
# other data migration commands ###########################################
@@ -1529,6 +1526,10 @@
def cmd_reactivate_verification_hooks(self):
self.session.enable_hook_categories('integrity')
+ @deprecated("[3.15] use rename_relation_type(oldname, newname)")
+ def cmd_rename_relation(self, oldname, newname, commit=True):
+ self.cmd_rename_relation_type(oldname, newname, commit)
+
class ForRqlIterator:
"""specific rql iterator to make the loop skipable"""
--- a/server/pool.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/pool.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/server/querier.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/querier.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -26,21 +26,26 @@
from itertools import repeat
from logilab.common.compat import any
-from rql import RQLSyntaxError
+from rql import RQLSyntaxError, CoercionError
from rql.stmts import Union, Select
+from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj
from rql.nodes import (Relation, VariableRef, Constant, SubQuery, Function,
Exists, Not)
+from yams import BASE_TYPES
from cubicweb import ValidationError, Unauthorized, QueryError, UnknownEid
-from cubicweb import server, typed_eid
+from cubicweb import Binary, server, typed_eid
from cubicweb.rset import ResultSet
-from cubicweb.utils import QueryCache
+from cubicweb.utils import QueryCache, RepeatList
from cubicweb.server.utils import cleanup_solutions
from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
from cubicweb.server.edition import EditedEntity
-from cubicweb.server.session import security_enabled
+
+
+ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
+
def empty_rset(rql, args, rqlst=None):
"""build an empty result set object"""
@@ -256,7 +261,7 @@
cached = True
else:
noinvariant = set()
- with security_enabled(self.session, read=False):
+ with self.session.security_enabled(read=False):
self._insert_security(union, noinvariant)
if key is not None:
self.session.transaction_data[key] = (union, self.args)
@@ -417,7 +422,7 @@
if rqlexpr.check(session, eid):
break
else:
- raise Unauthorized()
+ raise Unauthorized('No read acces on %r with eid %i.' % (var, eid))
restricted_vars.update(localcheck)
localchecks.setdefault(tuple(localcheck.iteritems()), []).append(solution)
# raise Unautorized exception if the user can't access to any solution
@@ -723,7 +728,7 @@
rqlst = rqlst.copy()
self._annotate(rqlst)
if args:
- # different SQL generated when some argument is None or not (IS
+ # different SQL generated when some argument is None or not (IS
# NULL). This should be considered when computing sql cache key
cachekey += tuple(sorted([k for k,v in args.iteritems()
if v is None]))
@@ -751,14 +756,22 @@
if build_descr:
if rqlst.TYPE == 'select':
# sample selection
- descr = session.build_description(orig_rqlst, args, results)
+ if len(rqlst.children) == 1 and len(rqlst.children[0].solutions) == 1:
+ # easy, all lines are identical
+ selected = rqlst.children[0].selection
+ solution = rqlst.children[0].solutions[0]
+ description = _make_description(selected, args, solution)
+ descr = RepeatList(len(results), tuple(description))
+ else:
+ # hard, delegate the work :o)
+ descr = manual_build_descr(session, rqlst, args, results)
elif rqlst.TYPE == 'insert':
# on insert plan, some entities may have been auto-casted,
# so compute description manually even if there is only
# one solution
basedescr = [None] * len(plan.selected)
todetermine = zip(xrange(len(plan.selected)), repeat(False))
- descr = session._build_descr(results, basedescr, todetermine)
+ descr = _build_descr(session, results, basedescr, todetermine)
# FIXME: get number of affected entities / relations on non
# selection queries ?
# return a result set object
@@ -772,3 +785,77 @@
from cubicweb import set_log_methods
LOGGER = getLogger('cubicweb.querier')
set_log_methods(QuerierHelper, LOGGER)
+
+
+def manual_build_descr(tx, rqlst, args, result):
+ """build a description for a given result by analysing each row
+
+ XXX could probably be done more efficiently during execution of query
+ """
+ # not so easy, looks for variable which changes from one solution
+ # to another
+ unstables = rqlst.get_variable_indices()
+ basedescr = []
+ todetermine = []
+ for i in xrange(len(rqlst.children[0].selection)):
+ ttype = _selection_idx_type(i, rqlst, args)
+ if ttype is None or ttype == 'Any':
+ ttype = None
+ isfinal = True
+ else:
+ isfinal = ttype in BASE_TYPES
+ if ttype is None or i in unstables:
+ basedescr.append(None)
+ todetermine.append( (i, isfinal) )
+ else:
+ basedescr.append(ttype)
+ if not todetermine:
+ return RepeatList(len(result), tuple(basedescr))
+ return _build_descr(tx, result, basedescr, todetermine)
+
+def _build_descr(tx, result, basedescription, todetermine):
+ description = []
+ etype_from_eid = tx.describe
+ todel = []
+ for i, row in enumerate(result):
+ row_descr = basedescription[:]
+ for index, isfinal in todetermine:
+ value = row[index]
+ if value is None:
+ # None value inserted by an outer join, no type
+ row_descr[index] = None
+ continue
+ if isfinal:
+ row_descr[index] = etype_from_pyobj(value)
+ else:
+ try:
+ row_descr[index] = etype_from_eid(value)[0]
+ except UnknownEid:
+ tx.error('wrong eid %s in repository, you should '
+ 'db-check the database' % value)
+ todel.append(i)
+ break
+ else:
+ description.append(tuple(row_descr))
+ for i in reversed(todel):
+ del result[i]
+ return description
+
+def _make_description(selected, args, solution):
+ """return a description for a result set"""
+ description = []
+ for term in selected:
+ description.append(term.get_type(solution, args))
+ return description
+
+def _selection_idx_type(i, rqlst, args):
+ """try to return type of term at index `i` of the rqlst's selection"""
+ for select in rqlst.children:
+ term = select.selection[i]
+ for solution in select.solutions:
+ try:
+ ttype = term.get_type(solution, args)
+ if ttype is not None:
+ return ttype
+ except CoercionError:
+ return None
--- a/server/repository.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/repository.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -56,8 +56,7 @@
RepositoryError, UniqueTogetherError, typed_eid, onevent)
from cubicweb import cwvreg, schema, server
from cubicweb.server import ShuttingDown, utils, hook, pool, querier, sources
-from cubicweb.server.session import Session, InternalSession, InternalManager, \
- security_enabled
+from cubicweb.server.session import Session, InternalSession, InternalManager
from cubicweb.server.ssplanner import EditedEntity
NO_CACHE_RELATIONS = set( [('owned_by', 'object'),
@@ -65,7 +64,7 @@
('cw_source', 'object'),
])
-def prefill_entity_caches(entity, relations):
+def prefill_entity_caches(entity):
session = entity._cw
# prefill entity relation caches
for rschema in entity.e_schema.subject_relations():
@@ -109,17 +108,38 @@
# * we don't want read permissions to be applied but we want delete
# permission to be checked
if card[0] in '1?':
- with security_enabled(session, read=False):
+ with session.security_enabled(read=False):
session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
'NOT Y eid %%(y)s' % rtype,
{'x': eidfrom, 'y': eidto})
if card[1] in '1?':
- with security_enabled(session, read=False):
+ with session.security_enabled(read=False):
session.execute('DELETE X %s Y WHERE Y eid %%(y)s, '
'NOT X eid %%(x)s' % rtype,
{'x': eidfrom, 'y': eidto})
+def preprocess_inlined_relations(session, entity):
+ """when an entity is added, check if it has some inlined relation which
+ requires to be extrated for proper call hooks
+ """
+ relations = []
+ activeintegrity = session.is_hook_category_activated('activeintegrity')
+ eschema = entity.e_schema
+ for attr in entity.cw_edited.iterkeys():
+ rschema = eschema.subjrels[attr]
+ if not rschema.final: # inlined relation
+ value = entity.cw_edited[attr]
+ relations.append((attr, value))
+ session.update_rel_cache_add(entity.eid, attr, value)
+ rdef = session.rtype_eids_rdef(attr, entity.eid, value)
+ if rdef.cardinality[1] in '1?' and activeintegrity:
+ with session.security_enabled(read=False):
+ session.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
+ {'x': entity.eid, 'y': value})
+ return relations
+
+
class NullEventBus(object):
def publish(self, msg):
pass
@@ -141,19 +161,22 @@
XXX protect pyro access
"""
- def __init__(self, config, vreg=None):
+ def __init__(self, config, tasks_manager=None, vreg=None):
self.config = config
if vreg is None:
vreg = cwvreg.CWRegistryStore(config)
self.vreg = vreg
+ self._tasks_manager = tasks_manager
+
self.pyro_registered = False
self.pyro_uri = None
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
- self._looping_tasks = []
# list of running threads
self._running_threads = []
# initial schema, should be build or replaced latter
@@ -177,6 +200,9 @@
self.init_cnxset_pool()
@onevent('after-registry-reload', self)
def fix_user_classes(self):
+ # After registery reload the 'CWUser' class used for CWEtype
+ # changed. To 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):
@@ -191,8 +217,8 @@
# information (eg dump/restore/...)
config._cubes = ()
# only load hooks and entity classes in the registry
- config.__class__.cube_appobject_path = set(('hooks', 'entities'))
- config.__class__.cubicweb_appobject_path = set(('hooks', 'entities'))
+ config.cube_appobject_path = set(('hooks', 'entities'))
+ config.cubicweb_appobject_path = set(('hooks', 'entities'))
self.set_schema(config.load_schema())
config['connections-pool-size'] = 1
# will be reinitialized later from cubes found in the database
@@ -200,16 +226,10 @@
elif config.creating:
# repository creation
config.bootstrap_cubes()
- self.set_schema(config.load_schema(), resetvreg=False)
- # need to load the Any and CWUser entity types
- etdirectory = join(CW_SOFTWARE_ROOT, 'entities')
- self.vreg.init_registration([etdirectory])
- for modname in ('__init__', 'authobjs', 'wfobjs'):
- self.vreg.load_file(join(etdirectory, '%s.py' % modname),
- 'cubicweb.entities.%s' % modname)
- hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
- self.vreg.load_file(join(hooksdirectory, 'metadata.py'),
- 'cubicweb.hooks.metadata')
+ # trigger vreg initialisation of entity classes
+ config.cubicweb_appobject_path = set(('hooks', 'entities'))
+ config.cube_appobject_path = set(('hooks', 'entities'))
+ self.set_schema(config.load_schema())
elif config.read_instance_schema:
# normal start: load the instance schema from the database
self.fill_schema()
@@ -247,8 +267,7 @@
or not 'CWSource' in self.schema: # # 3.10 migration
self.system_source.init_creating()
return
- session = self.internal_session()
- try:
+ with self.internal_session() as session:
# FIXME: sources should be ordered (add_entity priority)
for sourceent in session.execute(
'Any S, SN, SA, SC WHERE S is_instance_of CWSource, '
@@ -259,8 +278,6 @@
self.system_source.init(True, sourceent)
continue
self.add_source(sourceent, add_to_cnxsets=False)
- finally:
- session.close()
def _clear_planning_caches(self):
for cache in ('source_defs', 'is_multi_sources_relation',
@@ -324,14 +341,13 @@
self.schema = schema
def fill_schema(self):
- """lod schema from the repository"""
+ """load schema from the repository"""
from cubicweb.server.schemaserial import deserialize_schema
self.info('loading schema from the repository')
appschema = schema.CubicWebSchema(self.config.appid)
self.set_schema(self.config.load_bootstrap_schema(), resetvreg=False)
self.debug('deserializing db schema into %s %#x', appschema.name, id(appschema))
- session = self.internal_session()
- try:
+ with self.internal_session() as session:
try:
deserialize_schema(appschema, session)
except BadSchemaDefinition:
@@ -342,11 +358,15 @@
raise Exception('Is the database initialised ? (cause: %s)' %
(ex.args and ex.args[0].strip() or 'unknown')), \
None, sys.exc_info()[-1]
- finally:
- session.close()
self.set_schema(appschema)
- def start_looping_tasks(self):
+
+ def _prepare_startup(self):
+ """Prepare "Repository as a server" for startup.
+
+ * 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
@@ -355,15 +375,23 @@
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)
- self.looping_task(cleanup_session_interval, self.clean_sessions)
- assert isinstance(self._looping_tasks, list), 'already started'
- for i, (interval, func, args) in enumerate(self._looping_tasks):
- self._looping_tasks[i] = task = utils.LoopTask(self, interval, func, args)
- self.info('starting task %s with interval %.2fs', task.name,
- interval)
- task.start()
- # ensure no tasks will be further added
- self._looping_tasks = tuple(self._looping_tasks)
+ 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.
+
+ 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, "This Repository is not intended to be used as a server"
+ self._tasks_manager.start()
def looping_task(self, interval, func, *args):
"""register a function to be called every `interval` seconds.
@@ -371,15 +399,12 @@
looping tasks can only be registered during repository initialization,
once done this method will fail.
"""
- try:
- self._looping_tasks.append( (interval, func, args) )
- except AttributeError:
- raise RuntimeError("can't add looping task once the repository is started")
+ assert self._tasks_manager is not None, "This Repository is not intended to be used as a server"
+ self._tasks_manager.add_looping_task(interval, func, *args)
def threaded_task(self, func):
"""start function in a separated thread"""
- t = utils.RepoThread(func, self._running_threads)
- t.start()
+ utils.RepoThread(func, self._running_threads).start()
#@locked
def _get_cnxset(self):
@@ -407,21 +432,21 @@
connections
"""
assert not self.shutting_down, 'already shutting down'
+ if not (self.config.creating or self.config.repairing
+ or self.config.quick_start):
+ # then, the system source is still available
+ self.hm.call_hooks('before_server_shutdown', repo=self)
self.shutting_down = True
self.system_source.shutdown()
- if isinstance(self._looping_tasks, tuple): # if tasks have been started
- for looptask in self._looping_tasks:
- self.info('canceling task %s...', looptask.name)
- looptask.cancel()
- looptask.join()
- self.info('task %s finished', looptask.name)
+ 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)
for thread in self._running_threads:
self.info('waiting thread %s...', thread.getName())
thread.join()
self.info('thread %s finished', thread.getName())
- if not (self.config.creating or self.config.repairing
- or self.config.quick_start):
- self.hm.call_hooks('server_shutdown', repo=self)
self.close_sessions()
while not self._cnxsets_pool.empty():
cnxset = self._cnxsets_pool.get_nowait()
@@ -514,7 +539,8 @@
results['sql_no_cache'] = self.system_source.no_cache
results['nb_open_sessions'] = len(self._sessions)
results['nb_active_threads'] = threading.activeCount()
- results['looping_tasks'] = ', '.join(str(t) for t in self._looping_tasks)
+ looping_tasks = self._tasks_manager._looping_tasks
+ results['looping_tasks'] = ', '.join(str(t) for t in looping_tasks)
results['available_cnxsets'] = self._cnxsets_pool.qsize()
results['threads'] = ', '.join(sorted(str(t) for t in threading.enumerate()))
return results
@@ -612,8 +638,7 @@
"""
from logilab.common.changelog import Version
vcconf = {}
- session = self.internal_session()
- try:
+ with self.internal_session() as session:
for pk, version in session.execute(
'Any K,V WHERE P is CWProperty, P value V, P pkey K, '
'P pkey ~="system.version.%"', build_descr=False):
@@ -632,8 +657,6 @@
msg = ('instance has %s version %s but %s '
'is installed. Run "cubicweb-ctl upgrade".')
raise ExecutionError(msg % (cube, version, fsversion))
- finally:
- session.close()
return vcconf
@cached
@@ -654,14 +677,11 @@
This is a public method, not requiring a session id.
"""
- session = self.internal_session()
- try:
+ with self.internal_session() as session:
# don't use session.execute, we don't want rset.req set
return self.querier.execute(session, 'Any K,V WHERE P is CWProperty,'
'P pkey K, P value V, NOT P for_user U',
build_descr=False)
- finally:
- session.close()
# XXX protect this method: anonymous should be allowed and registration
# plugged
@@ -670,10 +690,9 @@
given password. This method is designed to be used for anonymous
registration on public web site.
"""
- session = self.internal_session()
- # for consistency, keep same error as unique check hook (although not required)
- errmsg = session._('the value "%s" is already used, use another one')
- try:
+ with self.internal_session() as session:
+ # for consistency, keep same error as unique check hook (although not required)
+ errmsg = session._('the value "%s" is already used, use another one')
if (session.execute('CWUser X WHERE X login %(login)s', {'login': login},
build_descr=False)
or session.execute('CWUser X WHERE X use_email C, C address %(login)s',
@@ -700,8 +719,6 @@
'U primary_email X, U use_email X '
'WHERE U login %(login)s', d, build_descr=False)
session.commit()
- finally:
- session.close()
return True
def find_users(self, fetch_attrs, **query_attrs):
@@ -724,8 +741,7 @@
for k in chain(fetch_attrs, query_attrs.iterkeys()):
if k not in cwuserattrs:
raise Exception('bad input for find_user')
- session = self.internal_session()
- try:
+ with self.internal_session() as session:
varmaker = rqlvar_maker()
vars = [(attr, varmaker.next()) for attr in fetch_attrs]
rql = 'Any %s WHERE X is CWUser, ' % ','.join(var[1] for var in vars)
@@ -734,8 +750,6 @@
for attr in query_attrs.iterkeys()),
query_attrs)
return rset.rows
- finally:
- session.close()
def connect(self, login, **kwargs):
"""open a connection for a given user
@@ -746,14 +760,11 @@
raise `AuthenticationError` if the authentication failed
raise `ConnectionError` if we can't open a connection
"""
+ cnxprops = kwargs.pop('cnxprops', None)
# use an internal connection
- session = self.internal_session()
- # try to get a user object
- cnxprops = kwargs.pop('cnxprops', None)
- try:
+ with self.internal_session() as session:
+ # try to get a user object
user = self.authenticate_user(session, login, **kwargs)
- finally:
- session.close()
session = Session(user, self, cnxprops)
user._cw = user.cw_rset.req = session
user.cw_clear_relation_cache()
@@ -878,28 +889,34 @@
del self._sessions[sessionid]
self.info('closed session %s for user %s', sessionid, session.user.login)
+ def call_service(self, sessionid, regid, async, **kwargs):
+ """
+ See :class:`cubicweb.dbapi.Connection.call_service`
+ and :class:`cubicweb.server.Service`
+ """
+ def task():
+ session = self._get_session(sessionid, setcnxset=True)
+ service = session.vreg['services'].select(regid, session, **kwargs)
+ try:
+ return service.call(**kwargs)
+ finally:
+ session.rollback() # free cnxset
+ if async:
+ self.info('calling service %s asynchronously', regid)
+ self.threaded_task(task)
+ else:
+ self.info('calling service %s synchronously', regid)
+ return task()
+
def user_info(self, sessionid, props=None):
"""this method should be used by client to:
* check session id validity
* update user information on each user's request (i.e. groups and
custom properties)
"""
- session = self._get_session(sessionid, setcnxset=False)
- if props is not None:
- self.set_session_props(sessionid, props)
- user = session.user
+ user = self._get_session(sessionid, setcnxset=False).user
return user.eid, user.login, user.groups, user.properties
- def set_session_props(self, sessionid, props):
- """this method should be used by client to:
- * check session id validity
- * update user information on each user's request (i.e. groups and
- custom properties)
- """
- session = self._get_session(sessionid, setcnxset=False)
- for prop, value in props.items():
- session.change_property(prop, value)
-
def undoable_transactions(self, sessionid, ueid=None, txid=None,
**actionfilters):
"""See :class:`cubicweb.dbapi.Connection.undoable_transactions`"""
@@ -947,14 +964,11 @@
* list of (etype, eid) of entities of the given types which have been
deleted since the given timestamp
"""
- session = self.internal_session()
- updatetime = datetime.utcnow()
- try:
+ with self.internal_session() as session:
+ updatetime = datetime.utcnow()
modentities, delentities = self.system_source.modified_entities(
session, etypes, mtime)
return updatetime, modentities, delentities
- finally:
- session.close()
# session handling ########################################################
@@ -1205,7 +1219,7 @@
source = self.sources_by_eid[scleanup]
# delete remaining relations: if user can delete the entity, he can
# delete all its relations without security checking
- with security_enabled(session, read=False, write=False):
+ with session.security_enabled(read=False, write=False):
eid = entity.eid
for rschema, _, role in entity.e_schema.relation_definitions():
rtype = rschema.type
@@ -1247,7 +1261,7 @@
source = self.sources_by_eid[scleanup]
# delete remaining relations: if user can delete the entity, he can
# delete all its relations without security checking
- with security_enabled(session, read=False, write=False):
+ with session.security_enabled(read=False, write=False):
in_eids = ','.join([str(_e.eid) for _e in entities])
for rschema, _, role in entities[0].e_schema.relation_definitions():
rtype = rschema.type
@@ -1339,7 +1353,6 @@
entity._cw_is_saved = False # entity has an eid but is not yet saved
# init edited_attributes before calling before_add_entity hooks
entity.cw_edited = edited
- eschema = entity.e_schema
source = self.locate_etype_source(entity.__regid__)
# allocate an eid to the entity before calling hooks
entity.eid = self.system_source.create_eid(session)
@@ -1347,22 +1360,10 @@
extid = self.init_entity_caches(session, entity, source)
if server.DEBUG & server.DBG_REPO:
print 'ADD entity', self, entity.__regid__, entity.eid, edited
- relations = []
- prefill_entity_caches(entity, relations)
+ prefill_entity_caches(entity)
if source.should_call_hooks:
self.hm.call_hooks('before_add_entity', session, entity=entity)
- activintegrity = session.is_hook_category_activated('activeintegrity')
- for attr in edited.iterkeys():
- rschema = eschema.subjrels[attr]
- if not rschema.final: # inlined relation
- value = edited[attr]
- relations.append((attr, value))
- session.update_rel_cache_add(entity.eid, attr, value)
- rdef = session.rtype_eids_rdef(attr, entity.eid, value)
- if rdef.cardinality[1] in '1?' and activintegrity:
- with security_enabled(session, read=False):
- session.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
- {'x': entity.eid, 'y': value})
+ relations = preprocess_inlined_relations(session, entity)
edited.set_defaults()
if session.is_hook_category_activated('integrity'):
edited.check(creation=True)
@@ -1525,7 +1526,7 @@
activintegrity = session.is_hook_category_activated('activeintegrity')
for rtype, eids_subj_obj in relations.iteritems():
if server.DEBUG & server.DBG_REPO:
- for subjeid, objeid in relations:
+ for subjeid, objeid in eids_subj_obj:
print 'ADD relation', subjeid, rtype, objeid
for subjeid, objeid in eids_subj_obj:
source = self.locate_relation_source(session, subjeid, rtype, objeid)
@@ -1546,7 +1547,7 @@
rdef = session.rtype_eids_rdef(rtype, subjeid, objeid)
card = rdef.cardinality
if card[0] in '?1':
- with security_enabled(session, read=False):
+ with session.security_enabled(read=False):
session.execute('DELETE X %s Y WHERE X eid %%(x)s, '
'NOT Y eid %%(y)s' % rtype,
{'x': subjeid, 'y': objeid})
@@ -1557,7 +1558,7 @@
continue
subjects[subjeid] = len(relations_by_rtype[rtype]) - 1
if card[1] in '?1':
- with security_enabled(session, read=False):
+ with session.security_enabled(read=False):
session.execute('DELETE X %s Y WHERE Y eid %%(y)s, '
'NOT X eid %%(x)s' % rtype,
{'x': subjeid, 'y': objeid})
@@ -1671,6 +1672,7 @@
# only defining here to prevent pylint from complaining
info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
+
def pyro_unregister(config):
"""unregister the repository from the pyro name server"""
from logilab.common.pyro_ext import ns_unregister
--- a/server/rqlannotation.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/rqlannotation.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -28,12 +28,13 @@
from rql.utils import common_parent
def _annotate_select(annotator, rqlst):
+ has_text_query = False
for subquery in rqlst.with_:
- annotator._annotate_union(subquery.query)
+ if annotator._annotate_union(subquery.query):
+ has_text_query = True
#if server.DEBUG:
# print '-------- sql annotate', repr(rqlst)
getrschema = annotator.schema.rschema
- has_text_query = False
need_distinct = rqlst.distinct
for rel in rqlst.iget_nodes(Relation):
if getrschema(rel.r_type).symmetric and not isinstance(rel.parent, Exists):
@@ -154,6 +155,11 @@
sstinfo['scope'] = common_parent(sstinfo['scope'], stinfo['scope']).scope
except CantSelectPrincipal:
stinfo['invariant'] = False
+ # see unittest_rqlannotation. test_has_text_security_cache_bug
+ # XXX probably more to do, but yet that work without more...
+ for col_alias in rqlst.aliases.itervalues():
+ if col_alias.stinfo.get('ftirels'):
+ has_text_query = True
rqlst.need_distinct = need_distinct
return has_text_query
@@ -272,8 +278,7 @@
def _annotate_union(self, union):
has_text_query = False
for select in union.children:
- htq = _annotate_select(self, select)
- if htq:
+ if _annotate_select(self, select):
has_text_query = True
return has_text_query
--- a/server/server.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/server.py Tue Oct 23 15:00:53 2012 +0200
@@ -26,6 +26,7 @@
from time import localtime, mktime
from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.server.utils import TasksManager
from cubicweb.server.repository import Repository
class Finished(Exception):
@@ -77,7 +78,7 @@
def __init__(self, config):
"""make the repository available as a PyRO object"""
self.config = config
- self.repo = Repository(config)
+ self.repo = Repository(config, TasksManager())
self.ns = None
self.quiting = None
# event queue
--- a/server/serverconfig.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/serverconfig.py Tue Oct 23 15:00:53 2012 +0200
@@ -140,12 +140,9 @@
'help': 'size of the parsed rql cache size.',
'group': 'main', 'level': 3,
}),
- ('undo-support',
- {'type' : 'string', 'default': '',
- 'help': 'string defining actions that will have undo support: \
-[C]reate [U]pdate [D]elete entities / [A]dd [R]emove relation. Leave it empty \
-for no undo support, set it to CUDAR for full undo support, or to DR for \
-support undoing of deletion only.',
+ ('undo-enabled',
+ {'type' : 'yn', 'default': False,
+ 'help': 'enable undo support',
'group': 'main', 'level': 3,
}),
('keep-transaction-lifetime',
@@ -207,7 +204,13 @@
and if not set, it will be choosen randomly',
'group': 'pyro', 'level': 3,
}),
-
+ # zmq services config
+ ('zmq-repository-address',
+ {'type' : 'string',
+ 'default': None,
+ 'help': 'ZMQ URI on which the repository will be bound to.',
+ 'group': 'zmq', 'level': 3,
+ }),
('zmq-address-sub',
{'type' : 'csv',
'default' : None,
--- a/server/serverctl.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/serverctl.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -35,6 +35,7 @@
from cubicweb.toolsutils import Command, CommandHandler, underline_title
from cubicweb.cwctl import CWCTL, check_options_consistency
from cubicweb.server import SOURCE_TYPES
+from cubicweb.server.repository import Repository
from cubicweb.server.serverconfig import (
USER_OPTIONS, ServerConfiguration, SourceConfiguration,
ask_source_config, generate_source_config)
@@ -446,7 +447,7 @@
get_connection(
system['db-driver'], database=system['db-name'],
host=system.get('db-host'), port=system.get('db-port'),
- user=system.get('db-user'), password=system.get('db-password'),
+ user=system.get('db-user') or '', password=system.get('db-password') or '',
**extra)
except Exception, ex:
raise ConfigurationError(
@@ -633,7 +634,7 @@
class StartRepositoryCommand(Command):
"""Start a CubicWeb RQL server for a given instance.
- The server will be accessible through pyro
+ The server will be remotely accessible through pyro or ZMQ
<instance>
the identifier of the instance to initialize.
@@ -650,12 +651,30 @@
'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
'help': 'debug if -D is set, error otherwise',
}),
+ ('address',
+ {'short': 'a', 'type': 'string', 'metavar': '<protocol>://<host>:<port>',
+ 'default': '',
+ 'help': ('specify a ZMQ URI on which to bind, or use "pyro://"'
+ 'to create a pyro-based repository'),
+ }),
)
+ def create_repo(self, config):
+ address = self['address']
+ if not address:
+ address = config.get('zmq-repository-address') or 'pyro://'
+ if address.startswith('pyro://'):
+ from cubicweb.server.server import RepositoryServer
+ return RepositoryServer(config), config['host']
+ else:
+ from cubicweb.server.utils import TasksManager
+ from cubicweb.server.cwzmq import ZMQRepositoryServer
+ repo = Repository(config, TasksManager())
+ return ZMQRepositoryServer(repo), address
+
def run(self, args):
from logilab.common.daemon import daemonize, setugid
from cubicweb.cwctl import init_cmdline_log_threshold
- from cubicweb.server.server import RepositoryServer
appid = args[0]
debug = self['debug']
if sys.platform == 'win32' and not debug:
@@ -665,7 +684,7 @@
config = ServerConfiguration.config_for(appid, debugmode=debug)
init_cmdline_log_threshold(config, self['loglevel'])
# create the server
- server = RepositoryServer(config)
+ server, address = self.create_repo(config)
# ensure the directory where the pid-file should be set exists (for
# instance /var/run/cubicweb may be deleted on computer restart)
pidfile = config['pid-file']
@@ -679,7 +698,7 @@
if uid is not None:
setugid(uid)
server.install_sig_handlers()
- server.connect(config['host'], 0)
+ server.connect(address)
server.run()
@@ -974,20 +993,24 @@
class RebuildFTICommand(Command):
"""Rebuild the full-text index of the system database of an instance.
- <instance>
+ <instance> [etype(s)]
the identifier of the instance to rebuild
+
+ If no etype is specified, cubicweb will reindex everything, otherwise
+ only specified etypes will be considered.
"""
name = 'db-rebuild-fti'
arguments = '<instance>'
- min_args = max_args = 1
+ min_args = 1
def run(self, args):
from cubicweb.server.checkintegrity import reindex_entities
- appid = args[0]
+ appid = args.pop(0)
+ etypes = args or None
config = ServerConfiguration.config_for(appid)
repo, cnx = repo_cnx(config)
session = repo._get_session(cnx.sessionid, setcnxset=True)
- reindex_entities(repo.schema, session)
+ reindex_entities(repo.schema, session, etypes=etypes)
cnx.commit()
--- a/server/session.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/session.py Tue Oct 23 15:00:53 2012 +0200
@@ -30,21 +30,16 @@
from logilab.common.deprecation import deprecated
from logilab.common.textutils import unormalize
from logilab.common.registry import objectify_predicate
-from rql import CoercionError
-from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj
-from yams import BASE_TYPES
-from cubicweb import Binary, UnknownEid, QueryError, schema
+from cubicweb import UnknownEid, QueryError, schema
from cubicweb.req import RequestSessionBase
from cubicweb.dbapi import ConnectionProperties
-from cubicweb.utils import make_uid, RepeatList
+from cubicweb.utils import make_uid
from cubicweb.rqlrewrite import RQLRewriter
from cubicweb.server import ShuttingDown
from cubicweb.server.edition import EditedEntity
-ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
-
NO_UNDO_TYPES = schema.SCHEMA_TYPES.copy()
NO_UNDO_TYPES.add('CWCache')
# is / is_instance_of are usually added by sql hooks except when using
@@ -55,25 +50,6 @@
NO_UNDO_TYPES.add('cw_source')
# XXX rememberme,forgotpwd,apycot,vcsfile
-def _make_description(selected, args, solution):
- """return a description for a result set"""
- description = []
- for term in selected:
- description.append(term.get_type(solution, args))
- return description
-
-def selection_idx_type(i, rqlst, args):
- """try to return type of term at index `i` of the rqlst's selection"""
- for select in rqlst.children:
- term = select.selection[i]
- for solution in select.solutions:
- try:
- ttype = term.get_type(solution, args)
- if ttype is not None:
- return ttype
- except CoercionError:
- return None
-
@objectify_predicate
def is_user_session(cls, req, **kwargs):
"""repository side only predicate returning 1 if the session is a regular
@@ -106,7 +82,8 @@
self.free_cnxset = free_cnxset
def __enter__(self):
- pass
+ # ensure session has a cnxset
+ self.session.set_cnxset()
def __exit__(self, exctype, exc, traceback):
if exctype:
@@ -131,6 +108,11 @@
with hooks_control(self.session, self.session.HOOKS_DENY_ALL, 'integrity'):
# ... do stuff with none but 'integrity' hooks activated
+
+ This is an internal api, you should rather use
+ :meth:`~cubicweb.server.session.Session.deny_all_hooks_but` or
+ :meth:`~cubicweb.server.session.Session.allow_all_hooks_but` session
+ methods.
"""
def __init__(self, session, mode, *categories):
self.session = session
@@ -240,7 +222,11 @@
:attr:`running_dbapi_query`, boolean flag telling if the executing query
is coming from a dbapi connection or is a query from within the repository
+
+ .. automethod:: cubicweb.server.session.deny_all_hooks_but
+ .. automethod:: cubicweb.server.session.all_all_hooks_but
"""
+ is_request = False
is_internal_session = False
def __init__(self, user, repo, cnxprops=None, _id=None):
@@ -252,20 +238,18 @@
self.cnxtype = cnxprops.cnxtype
self.timestamp = time()
self.default_mode = 'read'
- # support undo for Create Update Delete entity / Add Remove relation
+ # undo support
if repo.config.creating or repo.config.repairing or self.is_internal_session:
- self.undo_actions = ()
+ self.undo_actions = False
else:
- self.undo_actions = set(repo.config['undo-support'].upper())
- if self.undo_actions - set('CUDAR'):
- raise Exception('bad undo-support string in configuration')
+ self.undo_actions = repo.config['undo-enabled']
# short cut to querier .execute method
self._execute = repo.querier.execute
# shared data, used to communicate extra information between the client
# and the rql server
self.data = {}
# i18n initialization
- self.set_language(cnxprops.lang)
+ self.set_language(user.prefered_language())
# internals
self._tx_data = {}
self.__threaddata = threading.local()
@@ -463,28 +447,6 @@
self.cnxset.reconnect(source)
return source.doexec(self, sql, args, rollback=rollback_on_failure)
- def set_language(self, language):
- """i18n configuration for translation"""
- language = language or self.user.property_value('ui.language')
- try:
- gettext, pgettext = self.vreg.config.translations[language]
- self._ = self.__ = gettext
- self.pgettext = pgettext
- except KeyError:
- language = self.vreg.property_value('ui.language')
- try:
- gettext, pgettext = self.vreg.config.translations[language]
- self._ = self.__ = gettext
- self.pgettext = pgettext
- except KeyError:
- self._ = self.__ = unicode
- self.pgettext = lambda x, y: y
- self.lang = language
-
- def change_property(self, prop, value):
- assert prop == 'lang' # this is the only one changeable property for now
- self.set_language(value)
-
def deleted_in_transaction(self, eid):
"""return True if the entity of the given eid is being deleted in the
current transaction
@@ -508,7 +470,7 @@
DEFAULT_SECURITY = object() # evaluated to true by design
- def security_enabled(self, read=False, write=False):
+ def security_enabled(self, read=None, write=None):
return security_enabled(self, read=read, write=write)
def init_security(self, read, write):
@@ -847,6 +809,12 @@
else:
self.data[key] = value
+ # server-side service call #################################################
+
+ def call_service(self, regid, async=False, **kwargs):
+ return self.repo.call_service(self.id, regid, async, **kwargs)
+
+
# request interface #######################################################
@property
@@ -890,7 +858,7 @@
"""return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
metas = self.repo.type_and_source_from_eid(eid, self)
if asdict:
- return dict(zip(('type', 'source', 'extid', 'asource'), metas))
+ return dict(zip(('type', 'source', 'extid', 'asource'), metas))
# XXX :-1 for cw compat, use asdict=True for full information
return metas[:-1]
@@ -1118,9 +1086,8 @@
# undo support ############################################################
- def undoable_action(self, action, ertype):
- return action in self.undo_actions and not ertype in NO_UNDO_TYPES
- # XXX elif transaction on mark it partial
+ def ertype_supports_undo(self, ertype):
+ return self.undo_actions and ertype not in NO_UNDO_TYPES
def transaction_uuid(self, set=True):
try:
@@ -1148,71 +1115,6 @@
self._threaddata._rewriter = RQLRewriter(self)
return self._threaddata._rewriter
- def build_description(self, rqlst, args, result):
- """build a description for a given result"""
- if len(rqlst.children) == 1 and len(rqlst.children[0].solutions) == 1:
- # easy, all lines are identical
- selected = rqlst.children[0].selection
- solution = rqlst.children[0].solutions[0]
- description = _make_description(selected, args, solution)
- return RepeatList(len(result), tuple(description))
- # hard, delegate the work :o)
- return self.manual_build_descr(rqlst, args, result)
-
- def manual_build_descr(self, rqlst, args, result):
- """build a description for a given result by analysing each row
-
- XXX could probably be done more efficiently during execution of query
- """
- # not so easy, looks for variable which changes from one solution
- # to another
- unstables = rqlst.get_variable_indices()
- basedescr = []
- todetermine = []
- for i in xrange(len(rqlst.children[0].selection)):
- ttype = selection_idx_type(i, rqlst, args)
- if ttype is None or ttype == 'Any':
- ttype = None
- isfinal = True
- else:
- isfinal = ttype in BASE_TYPES
- if ttype is None or i in unstables:
- basedescr.append(None)
- todetermine.append( (i, isfinal) )
- else:
- basedescr.append(ttype)
- if not todetermine:
- return RepeatList(len(result), tuple(basedescr))
- return self._build_descr(result, basedescr, todetermine)
-
- def _build_descr(self, result, basedescription, todetermine):
- description = []
- etype_from_eid = self.describe
- todel = []
- for i, row in enumerate(result):
- row_descr = basedescription[:]
- for index, isfinal in todetermine:
- value = row[index]
- if value is None:
- # None value inserted by an outer join, no type
- row_descr[index] = None
- continue
- if isfinal:
- row_descr[index] = etype_from_pyobj(value)
- else:
- try:
- row_descr[index] = etype_from_eid(value)[0]
- except UnknownEid:
- self.error('wrong eid %s in repository, you should '
- 'db-check the database' % value)
- todel.append(i)
- break
- else:
- description.append(tuple(row_descr))
- for i in reversed(todel):
- del result[i]
- return description
-
# deprecated ###############################################################
@deprecated('[3.13] use getattr(session.rtype_eids_rdef(rtype, eidfrom, eidto), prop)')
@@ -1272,6 +1174,12 @@
if not safe:
self.disable_hook_categories('integrity')
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exctype, excvalue, tb):
+ self.close()
+
@property
def cnxset(self):
"""connections set, set according to transaction mode for each query"""
@@ -1305,6 +1213,9 @@
return 'en'
return None
+ def prefered_language(self, language=None):
+ # mock CWUser.prefered_language, mainly for testing purpose
+ return self.property_value('ui.language')
from logging import getLogger
from cubicweb import set_log_methods
--- a/server/sources/__init__.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/__init__.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/server/sources/datafeed.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/datafeed.py Tue Oct 23 15:00:53 2012 +0200
@@ -28,9 +28,9 @@
from cookielib import CookieJar
from lxml import etree
-from logilab.mtconverter import xml_escape
from cubicweb import RegistryNotFound, ObjectNotFound, ValidationError, UnknownEid
+from cubicweb.server.repository import preprocess_inlined_relations
from cubicweb.server.sources import AbstractSource
from cubicweb.appobject import AppObject
@@ -68,7 +68,7 @@
}),
('delete-entities',
{'type' : 'yn',
- 'default': True,
+ 'default': False,
'help': ('Should already imported entities not found anymore on the '
'external source be deleted?'),
'group': 'datafeed-source', 'level': 2,
@@ -80,6 +80,7 @@
'group': 'datafeed-source', 'level': 2,
}),
)
+
def __init__(self, repo, source_config, eid=None):
AbstractSource.__init__(self, repo, source_config, eid)
self.update_config(None, self.check_conf_dict(eid, source_config,
@@ -152,21 +153,24 @@
def update_latest_retrieval(self, session):
self.latest_retrieval = datetime.utcnow()
+ session.set_cnxset()
session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
{'x': self.eid, 'date': self.latest_retrieval})
+ session.commit()
def acquire_synchronization_lock(self, session):
# XXX race condition until WHERE of SET queries is executed using
# 'SELECT FOR UPDATE'
now = datetime.utcnow()
+ session.set_cnxset()
if not session.execute(
'SET X in_synchronization %(now)s WHERE X eid %(x)s, '
'X in_synchronization NULL OR X in_synchronization < %(maxdt)s',
{'x': self.eid, 'now': now, 'maxdt': now - self.max_lock_lifetime}):
self.error('concurrent synchronization detected, skip pull')
- session.commit(free_cnxset=False)
+ session.commit()
return False
- session.commit(free_cnxset=False)
+ session.commit()
return True
def release_synchronization_lock(self, session):
@@ -192,30 +196,22 @@
self.release_synchronization_lock(session)
def _pull_data(self, session, force=False, raise_on_error=False):
- if self.config['delete-entities']:
- myuris = self.source_cwuris(session)
- else:
- myuris = None
importlog = self.init_import_log(session)
+ myuris = self.source_cwuris(session)
parser = self._get_parser(session, sourceuris=myuris, import_log=importlog)
if self.process_urls(parser, self.urls, raise_on_error):
self.warning("some error occured, don't attempt to delete entities")
- elif self.config['delete-entities'] and myuris:
- byetype = {}
- for extid, (eid, etype) in myuris.iteritems():
- if parser.is_deleted(extid, etype, eid):
- byetype.setdefault(etype, []).append(str(eid))
- for etype, eids in byetype.iteritems():
- self.warning('delete %s %s entities', len(eids), etype)
- session.execute('DELETE %s X WHERE X eid IN (%s)'
- % (etype, ','.join(eids)))
+ else:
+ parser.handle_deletion(self.config, session, myuris)
self.update_latest_retrieval(session)
stats = parser.stats
if stats.get('created'):
importlog.record_info('added %s entities' % len(stats['created']))
if stats.get('updated'):
importlog.record_info('updated %s entities' % len(stats['updated']))
+ session.set_cnxset()
importlog.write_log(session, end_timestamp=self.latest_retrieval)
+ session.commit()
return stats
def process_urls(self, parser, urls, raise_on_error=False):
@@ -259,18 +255,27 @@
"""called by the repository after an entity stored here has been
inserted in the system table.
"""
+ relations = preprocess_inlined_relations(session, entity)
if session.is_hook_category_activated('integrity'):
entity.cw_edited.check(creation=True)
self.repo.system_source.add_entity(session, entity)
entity.cw_edited.saved = entity._cw_is_saved = True
sourceparams['parser'].after_entity_copy(entity, sourceparams)
+ # call hooks for inlined relations
+ call_hooks = self.repo.hm.call_hooks
+ if self.should_call_hooks:
+ for attr, value in relations:
+ call_hooks('before_add_relation', session,
+ eidfrom=entity.eid, rtype=attr, eidto=value)
+ call_hooks('after_add_relation', session,
+ eidfrom=entity.eid, rtype=attr, eidto=value)
def source_cwuris(self, session):
sql = ('SELECT extid, eid, type FROM entities, cw_source_relation '
'WHERE entities.eid=cw_source_relation.eid_from '
'AND cw_source_relation.eid_to=%s' % self.eid)
return dict((b64decode(uri), (eid, type))
- for uri, eid, type in session.system_sql(sql))
+ for uri, eid, type in session.system_sql(sql).fetchall())
def init_import_log(self, session, **kwargs):
dataimport = session.create_entity('CWDataImport', cw_import_of=self,
@@ -288,8 +293,7 @@
self.source = source
self.sourceuris = sourceuris
self.import_log = import_log
- self.stats = {'created': set(),
- 'updated': set()}
+ self.stats = {'created': set(), 'updated': set(), 'checked': set()}
def normalize_url(self, url):
from cubicweb.sobjects import URL_MAPPING # available after registration
@@ -350,7 +354,7 @@
self.sourceuris.pop(str(uri), None)
return session.entity_from_eid(eid, etype)
- def process(self, url, partialcommit=True):
+ def process(self, url, raise_on_error=False):
"""main callback: process the url"""
raise NotImplementedError
@@ -369,6 +373,9 @@
def notify_updated(self, entity):
return self.stats['updated'].add(entity.eid)
+ def notify_checked(self, entity):
+ return self.stats['checked'].add(entity.eid)
+
def is_deleted(self, extid, etype, eid):
"""return True if the entity of given external id, entity type and eid
is actually deleted. Always return True by default, put more sensible
@@ -376,22 +383,36 @@
"""
return True
+ def handle_deletion(self, config, session, myuris):
+ if config['delete-entities'] and myuris:
+ byetype = {}
+ for extid, (eid, etype) in myuris.iteritems():
+ if self.is_deleted(extid, etype, eid):
+ byetype.setdefault(etype, []).append(str(eid))
+ for etype, eids in byetype.iteritems():
+ self.warning('delete %s %s entities', len(eids), etype)
+ session.set_cnxset()
+ session.execute('DELETE %s X WHERE X eid IN (%s)'
+ % (etype, ','.join(eids)))
+ session.commit()
+
def update_if_necessary(self, entity, attrs):
- self.notify_updated(entity)
entity.complete(tuple(attrs))
# check modification date and compare attribute values to only update
# what's actually needed
+ self.notify_checked(entity)
mdate = attrs.get('modification_date')
if not mdate or mdate > entity.modification_date:
attrs = dict( (k, v) for k, v in attrs.iteritems()
if v != getattr(entity, k))
if attrs:
- entity.set_attributes(**attrs)
+ entity.cw_set(**attrs)
+ self.notify_updated(entity)
class DataFeedXMLParser(DataFeedParser):
- def process(self, url, raise_on_error=False, partialcommit=True):
+ def process(self, url, raise_on_error=False):
"""IDataFeedParser main entry point"""
try:
parsed = self.parse(url)
@@ -401,24 +422,30 @@
self.import_log.record_error(str(ex))
return True
error = False
+ # Check whether self._cw is a session or a connection
+ if getattr(self._cw, 'commit', None) is not None:
+ commit = self._cw.commit
+ set_cnxset = self._cw.set_cnxset
+ rollback = self._cw.rollback
+ else:
+ commit = self._cw.cnx.commit
+ set_cnxset = lambda: None
+ rollback = self._cw.cnx.rollback
for args in parsed:
try:
self.process_item(*args)
- if partialcommit:
- # commit+set_cnxset instead of commit(free_cnxset=False) to let
- # other a chance to get our connections set
- self._cw.commit()
- self._cw.set_cnxset()
+ # commit+set_cnxset instead of commit(free_cnxset=False) to let
+ # other a chance to get our connections set
+ commit()
+ set_cnxset()
except ValidationError, exc:
if raise_on_error:
raise
- if partialcommit:
- self.source.error('Skipping %s because of validation error %s' % (args, exc))
- self._cw.rollback()
- self._cw.set_cnxset()
- error = True
- else:
- raise
+ self.source.error('Skipping %s because of validation error %s'
+ % (args, exc))
+ rollback()
+ set_cnxset()
+ error = True
return error
def parse(self, url):
--- a/server/sources/ldapfeed.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/ldapfeed.py Tue Oct 23 15:00:53 2012 +0200
@@ -29,7 +29,7 @@
datafeed.DataFeedSource):
"""LDAP feed source"""
support_entities = {'CWUser': False}
- use_cwuri_as_url = True
+ use_cwuri_as_url = False
options = datafeed.DataFeedSource.options + ldaputils.LDAPSourceMixIn.options
@@ -43,4 +43,3 @@
def _entity_update(self, source_entity):
datafeed.DataFeedSource._entity_update(self, source_entity)
ldaputils.LDAPSourceMixIn._entity_update(self, source_entity)
-
--- a/server/sources/native.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/native.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -55,17 +55,16 @@
from yams.schema import role_name
from cubicweb import (UnknownEid, AuthenticationError, ValidationError, Binary,
- UniqueTogetherError)
+ UniqueTogetherError, QueryError, UndoTransactionException)
from cubicweb import transaction as tx, server, neg_role
from cubicweb.utils import QueryCache
from cubicweb.schema import VIRTUAL_RTYPES
from cubicweb.cwconfig import CubicWebNoAppConfiguration
from cubicweb.server import hook
-from cubicweb.server.utils import crypt_password, eschema_eid
+from cubicweb.server.utils import crypt_password, eschema_eid, verify_and_update
from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
from cubicweb.server.rqlannotation import set_qdata
from cubicweb.server.hook import CleanupDeletedEidsCacheOp
-from cubicweb.server.session import hooks_control, security_enabled
from cubicweb.server.edition import EditedEntity
from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
from cubicweb.server.sources.rql2sql import SQLGenerator
@@ -162,24 +161,24 @@
allownull = rdef.cardinality[0] != '1'
return coltype, allownull
-class UndoException(Exception):
+
+class _UndoException(Exception):
"""something went wrong during undoing"""
def __unicode__(self):
"""Called by the unicode builtin; should return a Unicode object
- Type of UndoException message must be `unicode` by design in CubicWeb.
+ Type of _UndoException message must be `unicode` by design in CubicWeb.
+ """
+ assert isinstance(self.args[0], unicode)
+ return self.args[0]
- .. warning::
- This method is not available in python2.5"""
- assert isinstance(self.message, unicode)
- return self.message
def _undo_check_relation_target(tentity, rdef, role):
"""check linked entity has not been redirected for this relation"""
card = rdef.role_cardinality(role)
if card in '?1' and tentity.related(rdef.rtype, role):
- raise UndoException(tentity._cw._(
+ raise _UndoException(tentity._cw._(
"Can't restore %(role)s relation %(rtype)s to entity %(eid)s which "
"is already linked using this relation.")
% {'role': neg_role(role),
@@ -192,7 +191,7 @@
try:
entities.append(session.entity_from_eid(eid))
except UnknownEid:
- raise UndoException(session._(
+ raise _UndoException(session._(
"Can't restore relation %(rtype)s, %(role)s entity %(eid)s"
" doesn't exist anymore.")
% {'role': session._(role),
@@ -203,7 +202,7 @@
rschema = session.vreg.schema.rschema(rtype)
rdef = rschema.rdefs[(sentity.__regid__, oentity.__regid__)]
except KeyError:
- raise UndoException(session._(
+ raise _UndoException(session._(
"Can't restore relation %(rtype)s between %(subj)s and "
"%(obj)s, that relation does not exists anymore in the "
"schema.")
@@ -614,12 +613,10 @@
etype = entities[0].__regid__
for attr, storage in self._storages.get(etype, {}).items():
for entity in entities:
- try:
+ if event == 'deleted':
+ storage.entity_deleted(entity, attr)
+ else:
edited = entity.cw_edited
- except AttributeError:
- assert event == 'deleted'
- getattr(storage, 'entity_deleted')(entity, attr)
- else:
if attr in edited:
handler = getattr(storage, 'entity_%s' % event)
to_restore = handler(entity, attr)
@@ -637,7 +634,7 @@
attrs = self.preprocess_entity(entity)
sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs)
self.doexec(session, sql, attrs)
- if session.undoable_action('C', entity.__regid__):
+ if session.ertype_supports_undo(entity.__regid__):
self._record_tx_action(session, 'tx_entity_actions', 'C',
etype=entity.__regid__, eid=entity.eid)
@@ -645,7 +642,7 @@
"""replace an entity in the source"""
with self._storage_handler(entity, 'updated'):
attrs = self.preprocess_entity(entity)
- if session.undoable_action('U', entity.__regid__):
+ if session.ertype_supports_undo(entity.__regid__):
changes = self._save_attrs(session, entity, attrs)
self._record_tx_action(session, 'tx_entity_actions', 'U',
etype=entity.__regid__, eid=entity.eid,
@@ -657,7 +654,7 @@
def delete_entity(self, session, entity):
"""delete an entity from the source"""
with self._storage_handler(entity, 'deleted'):
- if session.undoable_action('D', entity.__regid__):
+ if session.ertype_supports_undo(entity.__regid__):
attrs = [SQL_PREFIX + r.type
for r in entity.e_schema.subject_relations()
if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
@@ -672,14 +669,14 @@
def add_relation(self, session, subject, rtype, object, inlined=False):
"""add a relation to the source"""
self._add_relations(session, rtype, [(subject, object)], inlined)
- if session.undoable_action('A', rtype):
+ if session.ertype_supports_undo(rtype):
self._record_tx_action(session, 'tx_relation_actions', 'A',
eid_from=subject, rtype=rtype, eid_to=object)
def add_relations(self, session, rtype, subj_obj_list, inlined=False):
"""add a relations to the source"""
self._add_relations(session, rtype, subj_obj_list, inlined)
- if session.undoable_action('A', rtype):
+ if session.ertype_supports_undo(rtype):
for subject, object in subj_obj_list:
self._record_tx_action(session, 'tx_relation_actions', 'A',
eid_from=subject, rtype=rtype, eid_to=object)
@@ -712,7 +709,7 @@
"""delete a relation from the source"""
rschema = self.schema.rschema(rtype)
self._delete_relation(session, subject, rtype, object, rschema.inlined)
- if session.undoable_action('R', rtype):
+ if session.ertype_supports_undo(rtype):
self._record_tx_action(session, 'tx_relation_actions', 'R',
eid_from=subject, rtype=rtype, eid_to=object)
@@ -1157,16 +1154,18 @@
session.mode = 'write'
errors = []
session.transaction_data['undoing_uuid'] = txuuid
- with hooks_control(session, session.HOOKS_DENY_ALL,
- 'integrity', 'activeintegrity', 'undo'):
- with security_enabled(session, read=False):
+ with session.deny_all_hooks_but('integrity', 'activeintegrity', 'undo'):
+ with session.security_enabled(read=False):
for action in reversed(self.tx_actions(session, txuuid, False)):
undomethod = getattr(self, '_undo_%s' % action.action.lower())
errors += undomethod(session, action)
# remove the transactions record
self.doexec(session,
"DELETE FROM transactions WHERE tx_uuid='%s'" % txuuid)
- return errors
+ if errors:
+ raise UndoTransactionException(txuuid, errors)
+ else:
+ return
def start_undoable_transaction(self, session, uuid):
"""session callback to insert a transaction record in the transactions
@@ -1219,12 +1218,53 @@
try:
time, ueid = cu.fetchone()
except TypeError:
- raise tx.NoSuchTransaction()
+ raise tx.NoSuchTransaction(txuuid)
if not (session.user.is_in_group('managers')
or session.user.eid == ueid):
- raise tx.NoSuchTransaction()
+ raise tx.NoSuchTransaction(txuuid)
return time, ueid
+ def _reedit_entity(self, entity, changes, err):
+ session = entity._cw
+ eid = entity.eid
+ entity.cw_edited = edited = EditedEntity(entity)
+ # check for schema changes, entities linked through inlined relation
+ # still exists, rewrap binary values
+ eschema = entity.e_schema
+ getrschema = eschema.subjrels
+ for column, value in changes.items():
+ rtype = column[len(SQL_PREFIX):]
+ if rtype == "eid":
+ continue # XXX should even `eid` be stored in action changes?
+ try:
+ rschema = getrschema[rtype]
+ except KeyError:
+ err(session._("can't restore relation %(rtype)s of entity %(eid)s, "
+ "this relation does not exist in the schema anymore.")
+ % {'rtype': rtype, 'eid': eid})
+ if not rschema.final:
+ if not rschema.inlined:
+ assert value is None
+ # rschema is an inlined relation
+ elif value is not None:
+ # not a deletion: we must put something in edited
+ try:
+ entity._cw.entity_from_eid(value) # check target exists
+ edited[rtype] = value
+ except UnknownEid:
+ err(session._("can't restore entity %(eid)s of type %(eschema)s, "
+ "target of %(rtype)s (eid %(value)s) does not exist any longer")
+ % locals())
+ elif eschema.destination(rtype) in ('Bytes', 'Password'):
+ changes[column] = self._binary(value)
+ edited[rtype] = Binary(value)
+ elif isinstance(value, str):
+ edited[rtype] = unicode(value, session.encoding, 'replace')
+ else:
+ edited[rtype] = value
+ # This must only be done after init_entitiy_caches : defered in calling functions
+ # edited.check()
+
def _undo_d(self, session, action):
"""undo an entity deletion"""
errors = []
@@ -1239,31 +1279,10 @@
err("can't restore entity %s of type %s, type no more supported"
% (eid, etype))
return errors
- entity.cw_edited = edited = EditedEntity(entity)
- # check for schema changes, entities linked through inlined relation
- # still exists, rewrap binary values
- eschema = entity.e_schema
- getrschema = eschema.subjrels
- for column, value in action.changes.items():
- rtype = column[3:] # remove cw_ prefix
- try:
- rschema = getrschema[rtype]
- except KeyError:
- err(_("Can't restore relation %(rtype)s of entity %(eid)s, "
- "this relation does not exists anymore in the schema.")
- % {'rtype': rtype, 'eid': eid})
- if not rschema.final:
- assert value is None
- elif eschema.destination(rtype) in ('Bytes', 'Password'):
- action.changes[column] = self._binary(value)
- edited[rtype] = Binary(value)
- elif isinstance(value, str):
- edited[rtype] = unicode(value, session.encoding, 'replace')
- else:
- edited[rtype] = value
+ self._reedit_entity(entity, action.changes, err)
entity.eid = eid
session.repo.init_entity_caches(session, entity, self)
- edited.check()
+ entity.cw_edited.check()
self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
# restore the entity
action.changes['cw_eid'] = eid
@@ -1284,14 +1303,14 @@
subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
try:
sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj)
- except UndoException, ex:
+ except _UndoException, ex:
errors.append(unicode(ex))
else:
for role, entity in (('subject', sentity),
('object', oentity)):
try:
_undo_check_relation_target(entity, rdef, role)
- except UndoException, ex:
+ except _UndoException, ex:
errors.append(unicode(ex))
continue
if not errors:
@@ -1344,7 +1363,22 @@
def _undo_u(self, session, action):
"""undo an entity update"""
- return ['undoing of entity updating not yet supported.']
+ errors = []
+ err = errors.append
+ try:
+ entity = session.entity_from_eid(action.eid)
+ except UnknownEid:
+ err(session._("can't restore state of entity %s, it has been "
+ "deleted inbetween") % action.eid)
+ return errors
+ self._reedit_entity(entity, action.changes, err)
+ entity.cw_edited.check()
+ self.repo.hm.call_hooks('before_update_entity', session, entity=entity)
+ sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, action.changes,
+ ['cw_eid'])
+ self.doexec(session, sql, action.changes)
+ self.repo.hm.call_hooks('after_update_entity', session, entity=entity)
+ return errors
def _undo_a(self, session, action):
"""undo a relation addition"""
@@ -1352,7 +1386,7 @@
subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
try:
sentity, oentity, rdef = _undo_rel_info(session, subj, rtype, obj)
- except UndoException, ex:
+ except _UndoException, ex:
errors.append(unicode(ex))
else:
rschema = rdef.rtype
@@ -1561,9 +1595,10 @@
pass
class LoginPasswordAuthentifier(BaseAuthentifier):
- passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
- auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
- _sols = ({'X': 'CWUser', 'P': 'Password'},)
+ passwd_rql = 'Any P WHERE X is CWUser, X login %(login)s, X upassword P'
+ auth_rql = ('Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s, '
+ 'X cw_source S, S name "system"')
+ _sols = ({'X': 'CWUser', 'P': 'Password', 'S': 'CWSource'},)
def set_schema(self, schema):
"""set the instance'schema"""
@@ -1590,11 +1625,26 @@
# if pwd is None but a password is provided, something is wrong
raise AuthenticationError('bad password')
# passwords are stored using the Bytes type, so we get a StringIO
- args['pwd'] = Binary(crypt_password(password, pwd.getvalue()[:2]))
+ args['pwd'] = Binary(crypt_password(password, pwd.getvalue()))
# get eid from login and (crypted) password
rset = self.source.syntax_tree_search(session, self._auth_rqlst, args)
try:
- return rset[0][0]
+ user = rset[0][0]
+ # If the stored hash uses a deprecated scheme (e.g. DES or MD5 used
+ # before 3.14.7), update with a fresh one
+ if pwd.getvalue():
+ verify, newhash = verify_and_update(password, pwd.getvalue())
+ if not verify: # should not happen, but...
+ raise AuthenticationError('bad password')
+ if newhash:
+ session.system_sql("UPDATE %s SET %s=%%(newhash)s WHERE %s=%%(login)s" % (
+ SQL_PREFIX + 'CWUser',
+ SQL_PREFIX + 'upassword',
+ SQL_PREFIX + 'login'),
+ {'newhash': self.source._binary(newhash),
+ 'login': login})
+ session.commit(free_cnxset=False)
+ return user
except IndexError:
raise AuthenticationError('bad password')
@@ -1775,8 +1825,10 @@
versions = set(self._get_versions())
if file_versions != versions:
self.logger.critical('Unable to restore : versions do not match')
- self.logger.critical('Expected:\n%s', '\n'.join(list(sorted(versions))))
- self.logger.critical('Found:\n%s', '\n'.join(list(sorted(file_versions))))
+ self.logger.critical('Expected:\n%s', '\n'.join('%s : %s' % (cube, ver)
+ for cube, ver in sorted(versions)))
+ self.logger.critical('Found:\n%s', '\n'.join('%s : %s' % (cube, ver)
+ for cube, ver in sorted(file_versions)))
raise ValueError('Unable to restore : versions do not match')
table_chunks = {}
for name in archive.namelist():
--- a/server/sources/pyrorql.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/pyrorql.py Tue Oct 23 15:00:53 2012 +0200
@@ -21,298 +21,56 @@
_ = unicode
import threading
-from os.path import join
-from time import mktime
-from datetime import datetime
-from base64 import b64decode
-
from Pyro.errors import PyroError, ConnectionClosedError
from logilab.common.configuration import REQUIRED
-from logilab.common.optik_ext import check_yn
-
-from yams.schema import role_name
-
-from rql.nodes import Constant
-from rql.utils import rqlvar_maker
-from cubicweb import dbapi, server
-from cubicweb import ValidationError, BadConnectionId, UnknownEid, ConnectionError
-from cubicweb.schema import VIRTUAL_RTYPES
-from cubicweb.cwconfig import register_persistent_options
-from cubicweb.server.sources import (AbstractSource, ConnectionWrapper,
- TimedCache, dbg_st_search, dbg_results)
-from cubicweb.server.msplanner import neged_relation
+from cubicweb import dbapi
+from cubicweb import ConnectionError
+from cubicweb.server.sources import ConnectionWrapper
-def uidtype(union, col, etype, args):
- select, col = union.locate_subquery(col, etype, args)
- return getattr(select.selection[col], 'uidtype', None)
-
+from cubicweb.server.sources.remoterql import RemoteSource
-class ReplaceByInOperator(Exception):
- def __init__(self, eids):
- self.eids = eids
-
-class PyroRQLSource(AbstractSource):
+class PyroRQLSource(RemoteSource):
"""External repository source, using Pyro connection"""
- # boolean telling if modification hooks should be called when something is
- # modified in this source
- should_call_hooks = False
- # boolean telling if the repository should connect to this source during
- # migration
- connect_for_migration = False
+ CNX_TYPE = 'pyro'
- options = (
+ options = RemoteSource.options + (
# XXX pyro-ns host/port
('pyro-ns-id',
{'type' : 'string',
'default': REQUIRED,
'help': 'identifier of the repository in the pyro name server',
- 'group': 'pyro-source', 'level': 0,
- }),
- ('cubicweb-user',
- {'type' : 'string',
- 'default': REQUIRED,
- 'help': 'user to use for connection on the distant repository',
- 'group': 'pyro-source', 'level': 0,
- }),
- ('cubicweb-password',
- {'type' : 'password',
- 'default': '',
- 'help': 'user to use for connection on the distant repository',
- 'group': 'pyro-source', 'level': 0,
- }),
- ('base-url',
- {'type' : 'string',
- 'default': '',
- 'help': 'url of the web site for the distant repository, if you want '
- 'to generate external link to entities from this repository',
- 'group': 'pyro-source', 'level': 1,
- }),
- ('skip-external-entities',
- {'type' : 'yn',
- 'default': False,
- 'help': 'should entities not local to the source be considered or not',
- 'group': 'pyro-source', 'level': 0,
+ 'group': 'remote-source', 'level': 0,
}),
('pyro-ns-host',
{'type' : 'string',
'default': None,
'help': 'Pyro name server\'s host. If not set, default to the value \
from all_in_one.conf. It may contains port information using <host>:<port> notation.',
- 'group': 'pyro-source', 'level': 1,
+ 'group': 'remote-source', 'level': 1,
}),
('pyro-ns-group',
{'type' : 'string',
'default': None,
'help': 'Pyro name server\'s group where the repository will be \
registered. If not set, default to the value from all_in_one.conf.',
- 'group': 'pyro-source', 'level': 2,
+ 'group': 'remote-source', 'level': 2,
}),
- ('synchronization-interval',
- {'type' : 'time',
- 'default': '5min',
- 'help': 'interval between synchronization with the external \
-repository (default to 5 minutes).',
- 'group': 'pyro-source', 'level': 2,
- }),
-
)
- PUBLIC_KEYS = AbstractSource.PUBLIC_KEYS + ('base-url',)
- _conn = None
-
- def __init__(self, repo, source_config, eid=None):
- AbstractSource.__init__(self, repo, source_config, eid)
- self.update_config(None, self.check_conf_dict(eid, source_config,
- fail_if_unknown=False))
- self._query_cache = TimedCache(1800)
-
- def update_config(self, source_entity, processed_config):
- """update configuration from source entity"""
- # XXX get it through pyro if unset
- baseurl = processed_config.get('base-url')
- if baseurl and not baseurl.endswith('/'):
- processed_config['base-url'] += '/'
- self.config = processed_config
- self._skip_externals = processed_config['skip-external-entities']
- if source_entity is not None:
- self.latest_retrieval = source_entity.latest_retrieval
-
- def reset_caches(self):
- """method called during test to reset potential source caches"""
- self._query_cache = TimedCache(1800)
-
- def init(self, activated, source_entity):
- """method called by the repository once ready to handle request"""
- self.load_mapping(source_entity._cw)
- if activated:
- interval = self.config['synchronization-interval']
- self.repo.looping_task(interval, self.synchronize)
- self.repo.looping_task(self._query_cache.ttl.seconds/10,
- self._query_cache.clear_expired)
- self.latest_retrieval = source_entity.latest_retrieval
-
- def load_mapping(self, session=None):
- self.support_entities = {}
- self.support_relations = {}
- self.dont_cross_relations = set(('owned_by', 'created_by'))
- self.cross_relations = set()
- assert self.eid is not None
- self._schemacfg_idx = {}
- self._load_mapping(session)
-
- etype_options = set(('write',))
- rtype_options = set(('maycross', 'dontcross', 'write',))
-
- def _check_options(self, schemacfg, allowedoptions):
- if schemacfg.options:
- options = set(w.strip() for w in schemacfg.options.split(':'))
- else:
- options = set()
- if options - allowedoptions:
- options = ', '.join(sorted(options - allowedoptions))
- msg = _('unknown option(s): %s' % options)
- raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
- return options
-
- def add_schema_config(self, schemacfg, checkonly=False):
- """added CWSourceSchemaConfig, modify mapping accordingly"""
- try:
- ertype = schemacfg.schema.name
- except AttributeError:
- msg = schemacfg._cw._("attribute/relation can't be mapped, only "
- "entity and relation types")
- raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
- if schemacfg.schema.__regid__ == 'CWEType':
- options = self._check_options(schemacfg, self.etype_options)
- if not checkonly:
- self.support_entities[ertype] = 'write' in options
- else: # CWRType
- if ertype in ('is', 'is_instance_of', 'cw_source') or ertype in VIRTUAL_RTYPES:
- msg = schemacfg._cw._('%s relation should not be in mapped') % ertype
- raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
- options = self._check_options(schemacfg, self.rtype_options)
- if 'dontcross' in options:
- if 'maycross' in options:
- msg = schemacfg._("can't mix dontcross and maycross options")
- raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
- if 'write' in options:
- msg = schemacfg._("can't mix dontcross and write options")
- raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
- if not checkonly:
- self.dont_cross_relations.add(ertype)
- elif not checkonly:
- self.support_relations[ertype] = 'write' in options
- if 'maycross' in options:
- self.cross_relations.add(ertype)
- if not checkonly:
- # add to an index to ease deletion handling
- self._schemacfg_idx[schemacfg.eid] = ertype
-
- def del_schema_config(self, schemacfg, checkonly=False):
- """deleted CWSourceSchemaConfig, modify mapping accordingly"""
- if checkonly:
- return
- try:
- ertype = self._schemacfg_idx[schemacfg.eid]
- if ertype[0].isupper():
- del self.support_entities[ertype]
- else:
- if ertype in self.support_relations:
- del self.support_relations[ertype]
- if ertype in self.cross_relations:
- self.cross_relations.remove(ertype)
- else:
- self.dont_cross_relations.remove(ertype)
- except Exception:
- self.error('while updating mapping consequently to removal of %s',
- schemacfg)
-
- def local_eid(self, cnx, extid, session):
- etype, dexturi, dextid = cnx.describe(extid)
- if dexturi == 'system' or not (
- dexturi in self.repo.sources_by_uri or self._skip_externals):
- assert etype in self.support_entities, etype
- eid = self.repo.extid2eid(self, str(extid), etype, session)
- if eid > 0:
- return eid, True
- elif dexturi in self.repo.sources_by_uri:
- source = self.repo.sources_by_uri[dexturi]
- cnx = session.cnxset.connection(source.uri)
- eid = source.local_eid(cnx, dextid, session)[0]
- return eid, False
- return None, None
-
- def synchronize(self, mtime=None):
- """synchronize content known by this repository with content in the
- external repository
- """
- self.info('synchronizing pyro source %s', self.uri)
- cnx = self.get_connection()
- try:
- extrepo = cnx._repo
- except AttributeError:
- # fake connection wrapper returned when we can't connect to the
- # external source (hence we've no chance to synchronize...)
- return
- etypes = self.support_entities.keys()
- if mtime is None:
- mtime = self.latest_retrieval
- updatetime, modified, deleted = extrepo.entities_modified_since(
- etypes, mtime)
- self._query_cache.clear()
- repo = self.repo
- session = repo.internal_session()
- source = repo.system_source
- try:
- for etype, extid in modified:
- try:
- eid = self.local_eid(cnx, extid, session)[0]
- if eid is not None:
- rset = session.eid_rset(eid, etype)
- entity = rset.get_entity(0, 0)
- entity.complete(entity.e_schema.indexable_attributes())
- source.index_entity(session, entity)
- except Exception:
- self.exception('while updating %s with external id %s of source %s',
- etype, extid, self.uri)
- continue
- for etype, extid in deleted:
- try:
- eid = self.repo.extid2eid(self, str(extid), etype, session,
- insert=False)
- # entity has been deleted from external repository but is not known here
- if eid is not None:
- entity = session.entity_from_eid(eid, etype)
- repo.delete_info(session, entity, self.uri,
- scleanup=self.eid)
- except Exception:
- if self.repo.config.mode == 'test':
- raise
- self.exception('while updating %s with external id %s of source %s',
- etype, extid, self.uri)
- continue
- self.latest_retrieval = updatetime
- session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
- {'x': self.eid, 'date': self.latest_retrieval})
- session.commit()
- finally:
- session.close()
-
def _get_connection(self):
"""open and return a connection to the source"""
nshost = self.config.get('pyro-ns-host') or self.repo.config['pyro-ns-host']
nsgroup = self.config.get('pyro-ns-group') or self.repo.config['pyro-ns-group']
self.info('connecting to instance :%s.%s for user %s',
nsgroup, self.config['pyro-ns-id'], self.config['cubicweb-user'])
- #cnxprops = ConnectionProperties(cnxtype=self.config['cnx-type'])
return dbapi.connect(database=self.config['pyro-ns-id'],
login=self.config['cubicweb-user'],
password=self.config['cubicweb-password'],
host=nshost, group=nsgroup,
- setvreg=False) #cnxprops=cnxprops)
+ setvreg=False)
def get_connection(self):
try:
@@ -333,373 +91,9 @@
except AttributeError:
# inmemory connection
pass
- if not isinstance(cnx, ConnectionWrapper):
- try:
- cnx.check()
- return # ok
- except (BadConnectionId, ConnectionClosedError):
- pass
- # try to reconnect
- return self.get_connection()
-
- def syntax_tree_search(self, session, union, args=None, cachekey=None,
- varmap=None):
- assert dbg_st_search(self.uri, union, varmap, args, cachekey)
- rqlkey = union.as_string(kwargs=args)
try:
- results = self._query_cache[rqlkey]
- except KeyError:
- results = self._syntax_tree_search(session, union, args)
- self._query_cache[rqlkey] = results
- assert dbg_results(results)
- return results
-
- def _syntax_tree_search(self, session, union, args):
- """return result from this source for a rql query (actually from a rql
- syntax tree and a solution dictionary mapping each used variable to a
- possible type). If cachekey is given, the query necessary to fetch the
- results (but not the results themselves) may be cached using this key.
- """
- if not args is None:
- args = args.copy()
- # get cached cursor anyway
- cu = session.cnxset[self.uri]
- if cu is None:
- # this is a ConnectionWrapper instance
- msg = session._("can't connect to source %s, some data may be missing")
- session.set_shared_data('sources_error', msg % self.uri, txdata=True)
- return []
- translator = RQL2RQL(self)
- try:
- rql = translator.generate(session, union, args)
- except UnknownEid, ex:
- if server.DEBUG:
- print ' unknown eid', ex, 'no results'
- return []
- if server.DEBUG & server.DBG_RQL:
- print ' translated rql', rql
- try:
- rset = cu.execute(rql, args)
- except Exception, ex:
- self.exception(str(ex))
- msg = session._("error while querying source %s, some data may be missing")
- session.set_shared_data('sources_error', msg % self.uri, txdata=True)
- return []
- descr = rset.description
- if rset:
- needtranslation = []
- rows = rset.rows
- for i, etype in enumerate(descr[0]):
- if (etype is None or not self.schema.eschema(etype).final
- or uidtype(union, i, etype, args)):
- needtranslation.append(i)
- if needtranslation:
- cnx = session.cnxset.connection(self.uri)
- for rowindex in xrange(rset.rowcount - 1, -1, -1):
- row = rows[rowindex]
- localrow = False
- for colindex in needtranslation:
- if row[colindex] is not None: # optional variable
- eid, local = self.local_eid(cnx, row[colindex], session)
- if local:
- localrow = True
- if eid is not None:
- row[colindex] = eid
- else:
- # skip this row
- del rows[rowindex]
- del descr[rowindex]
- break
- else:
- # skip row if it only contains eids of entities which
- # are actually from a source we also know locally,
- # except if some args specified (XXX should actually
- # check if there are some args local to the source)
- if not (translator.has_local_eid or localrow):
- del rows[rowindex]
- del descr[rowindex]
- results = rows
- else:
- results = []
- return results
-
- def _entity_relations_and_kwargs(self, session, entity):
- relations = []
- kwargs = {'x': self.repo.eid2extid(self, entity.eid, session)}
- for key, val in entity.cw_attr_cache.iteritems():
- relations.append('X %s %%(%s)s' % (key, key))
- kwargs[key] = val
- return relations, kwargs
-
- def add_entity(self, session, entity):
- """add a new entity to the source"""
- raise NotImplementedError()
-
- def update_entity(self, session, entity):
- """update an entity in the source"""
- relations, kwargs = self._entity_relations_and_kwargs(session, entity)
- cu = session.cnxset[self.uri]
- cu.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs)
- self._query_cache.clear()
- entity.cw_clear_all_caches()
-
- def delete_entity(self, session, entity):
- """delete an entity from the source"""
- if session.deleted_in_transaction(self.eid):
- # source is being deleted, don't propagate
- self._query_cache.clear()
- return
- cu = session.cnxset[self.uri]
- cu.execute('DELETE %s X WHERE X eid %%(x)s' % entity.__regid__,
- {'x': self.repo.eid2extid(self, entity.eid, session)})
- self._query_cache.clear()
-
- def add_relation(self, session, subject, rtype, object):
- """add a relation to the source"""
- cu = session.cnxset[self.uri]
- cu.execute('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
- {'x': self.repo.eid2extid(self, subject, session),
- 'y': self.repo.eid2extid(self, object, session)})
- self._query_cache.clear()
- session.entity_from_eid(subject).cw_clear_all_caches()
- session.entity_from_eid(object).cw_clear_all_caches()
-
- def delete_relation(self, session, subject, rtype, object):
- """delete a relation from the source"""
- if session.deleted_in_transaction(self.eid):
- # source is being deleted, don't propagate
- self._query_cache.clear()
- return
- cu = session.cnxset[self.uri]
- cu.execute('DELETE X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
- {'x': self.repo.eid2extid(self, subject, session),
- 'y': self.repo.eid2extid(self, object, session)})
- self._query_cache.clear()
- session.entity_from_eid(subject).cw_clear_all_caches()
- session.entity_from_eid(object).cw_clear_all_caches()
-
-
-class RQL2RQL(object):
- """translate a local rql query to be executed on a distant repository"""
- def __init__(self, source):
- self.source = source
- self.repo = source.repo
- self.current_operator = None
-
- def _accept_children(self, node):
- res = []
- for child in node.children:
- rql = child.accept(self)
- if rql is not None:
- res.append(rql)
- return res
-
- def generate(self, session, rqlst, args):
- self._session = session
- self.kwargs = args
- self.need_translation = False
- self.has_local_eid = False
- return self.visit_union(rqlst)
-
- def visit_union(self, node):
- s = self._accept_children(node)
- if len(s) > 1:
- return ' UNION '.join('(%s)' % q for q in s)
- return s[0]
+ return super(PyroRQLSource, self).check_connection(cnx)
+ except ConnectionClosedError:
+ # try to reconnect
+ return self.get_connection()
- def visit_select(self, node):
- """return the tree as an encoded rql string"""
- self._varmaker = rqlvar_maker(defined=node.defined_vars.copy())
- self._const_var = {}
- if node.distinct:
- base = 'DISTINCT Any'
- else:
- base = 'Any'
- s = ['%s %s' % (base, ','.join(v.accept(self) for v in node.selection))]
- if node.groupby:
- s.append('GROUPBY %s' % ', '.join(group.accept(self)
- for group in node.groupby))
- if node.orderby:
- s.append('ORDERBY %s' % ', '.join(self.visit_sortterm(term)
- for term in node.orderby))
- if node.limit is not None:
- s.append('LIMIT %s' % node.limit)
- if node.offset:
- s.append('OFFSET %s' % node.offset)
- restrictions = []
- if node.where is not None:
- nr = node.where.accept(self)
- if nr is not None:
- restrictions.append(nr)
- if restrictions:
- s.append('WHERE %s' % ','.join(restrictions))
-
- if node.having:
- s.append('HAVING %s' % ', '.join(term.accept(self)
- for term in node.having))
- subqueries = []
- for subquery in node.with_:
- subqueries.append('%s BEING (%s)' % (','.join(ca.name for ca in subquery.aliases),
- self.visit_union(subquery.query)))
- if subqueries:
- s.append('WITH %s' % (','.join(subqueries)))
- return ' '.join(s)
-
- def visit_and(self, node):
- res = self._accept_children(node)
- if res:
- return ', '.join(res)
- return
-
- def visit_or(self, node):
- res = self._accept_children(node)
- if len(res) > 1:
- return ' OR '.join('(%s)' % rql for rql in res)
- elif res:
- return res[0]
- return
-
- def visit_not(self, node):
- rql = node.children[0].accept(self)
- if rql:
- return 'NOT (%s)' % rql
- return
-
- def visit_exists(self, node):
- rql = node.children[0].accept(self)
- if rql:
- return 'EXISTS(%s)' % rql
- return
-
- def visit_relation(self, node):
- try:
- if isinstance(node.children[0], Constant):
- # simplified rqlst, reintroduce eid relation
- try:
- restr, lhs = self.process_eid_const(node.children[0])
- except UnknownEid:
- # can safely skip not relation with an unsupported eid
- if neged_relation(node):
- return
- raise
- else:
- lhs = node.children[0].accept(self)
- restr = None
- except UnknownEid:
- # can safely skip not relation with an unsupported eid
- if neged_relation(node):
- return
- # XXX what about optional relation or outer NOT EXISTS()
- raise
- if node.optional in ('left', 'both'):
- lhs += '?'
- if node.r_type == 'eid' or not self.source.schema.rschema(node.r_type).final:
- self.need_translation = True
- self.current_operator = node.operator()
- if isinstance(node.children[0], Constant):
- self.current_etypes = (node.children[0].uidtype,)
- else:
- self.current_etypes = node.children[0].variable.stinfo['possibletypes']
- try:
- rhs = node.children[1].accept(self)
- except UnknownEid:
- # can safely skip not relation with an unsupported eid
- if neged_relation(node):
- return
- # XXX what about optional relation or outer NOT EXISTS()
- raise
- except ReplaceByInOperator, ex:
- rhs = 'IN (%s)' % ','.join(eid for eid in ex.eids)
- self.need_translation = False
- self.current_operator = None
- if node.optional in ('right', 'both'):
- rhs += '?'
- if restr is not None:
- return '%s %s %s, %s' % (lhs, node.r_type, rhs, restr)
- return '%s %s %s' % (lhs, node.r_type, rhs)
-
- def visit_comparison(self, node):
- if node.operator in ('=', 'IS'):
- return node.children[0].accept(self)
- return '%s %s' % (node.operator.encode(),
- node.children[0].accept(self))
-
- def visit_mathexpression(self, node):
- return '(%s %s %s)' % (node.children[0].accept(self),
- node.operator.encode(),
- node.children[1].accept(self))
-
- def visit_function(self, node):
- #if node.name == 'IN':
- res = []
- for child in node.children:
- try:
- rql = child.accept(self)
- except UnknownEid, ex:
- continue
- res.append(rql)
- if not res:
- raise ex
- return '%s(%s)' % (node.name, ', '.join(res))
-
- def visit_constant(self, node):
- if self.need_translation or node.uidtype:
- if node.type == 'Int':
- self.has_local_eid = True
- return str(self.eid2extid(node.value))
- if node.type == 'Substitute':
- key = node.value
- # ensure we have not yet translated the value...
- if not key in self._const_var:
- self.kwargs[key] = self.eid2extid(self.kwargs[key])
- self._const_var[key] = None
- self.has_local_eid = True
- return node.as_string()
-
- def visit_variableref(self, node):
- """get the sql name for a variable reference"""
- return node.name
-
- def visit_sortterm(self, node):
- if node.asc:
- return node.term.accept(self)
- return '%s DESC' % node.term.accept(self)
-
- def process_eid_const(self, const):
- value = const.eval(self.kwargs)
- try:
- return None, self._const_var[value]
- except Exception:
- var = self._varmaker.next()
- self.need_translation = True
- restr = '%s eid %s' % (var, self.visit_constant(const))
- self.need_translation = False
- self._const_var[value] = var
- return restr, var
-
- def eid2extid(self, eid):
- try:
- return self.repo.eid2extid(self.source, eid, self._session)
- except UnknownEid:
- operator = self.current_operator
- if operator is not None and operator != '=':
- # deal with query like "X eid > 12"
- #
- # The problem is that eid order in the external source may
- # differ from the local source
- #
- # So search for all eids from this source matching the condition
- # locally and then to replace the "> 12" branch by "IN (eids)"
- #
- # XXX we may have to insert a huge number of eids...)
- sql = "SELECT extid FROM entities WHERE source='%s' AND type IN (%s) AND eid%s%s"
- etypes = ','.join("'%s'" % etype for etype in self.current_etypes)
- cu = self._session.system_sql(sql % (self.source.uri, etypes,
- operator, eid))
- # XXX buggy cu.rowcount which may be zero while there are some
- # results
- rows = cu.fetchall()
- if rows:
- raise ReplaceByInOperator((b64decode(r[0]) for r in rows))
- raise
-
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/sources/remoterql.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,670 @@
+# copyright 2003-2012 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/>.
+"""Source to query another RQL remote repository"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from os.path import join
+from base64 import b64decode
+
+from logilab.common.configuration import REQUIRED
+
+from yams.schema import role_name
+
+from rql.nodes import Constant
+from rql.utils import rqlvar_maker
+
+from cubicweb import dbapi, server
+from cubicweb import ValidationError, BadConnectionId, UnknownEid
+from cubicweb.schema import VIRTUAL_RTYPES
+from cubicweb.server.sources import (AbstractSource, ConnectionWrapper,
+ TimedCache, dbg_st_search, dbg_results)
+from cubicweb.server.msplanner import neged_relation
+
+def uidtype(union, col, etype, args):
+ select, col = union.locate_subquery(col, etype, args)
+ return getattr(select.selection[col], 'uidtype', None)
+
+
+class ReplaceByInOperator(Exception):
+ def __init__(self, eids):
+ self.eids = eids
+
+class RemoteSource(AbstractSource):
+ """Generic external repository source"""
+
+ CNX_TYPE = None # Must be ovewritted !
+
+ # boolean telling if modification hooks should be called when something is
+ # modified in this source
+ should_call_hooks = False
+ # boolean telling if the repository should connect to this source during
+ # migration
+ connect_for_migration = False
+
+ options = (
+
+ ('cubicweb-user',
+ {'type' : 'string',
+ 'default': REQUIRED,
+ 'help': 'user to use for connection on the distant repository',
+ 'group': 'remote-source', 'level': 0,
+ }),
+ ('cubicweb-password',
+ {'type' : 'password',
+ 'default': '',
+ 'help': 'user to use for connection on the distant repository',
+ 'group': 'remote-source', 'level': 0,
+ }),
+ ('base-url',
+ {'type' : 'string',
+ 'default': '',
+ 'help': 'url of the web site for the distant repository, if you want '
+ 'to generate external link to entities from this repository',
+ 'group': 'remote-source', 'level': 1,
+ }),
+ ('skip-external-entities',
+ {'type' : 'yn',
+ 'default': False,
+ 'help': 'should entities not local to the source be considered or not',
+ 'group': 'remote-source', 'level': 0,
+ }),
+ ('synchronization-interval',
+ {'type' : 'time',
+ 'default': '5min',
+ 'help': 'interval between synchronization with the external \
+repository (default to 5 minutes).',
+ 'group': 'remote-source', 'level': 2,
+ }))
+
+ PUBLIC_KEYS = AbstractSource.PUBLIC_KEYS + ('base-url',)
+
+ _conn = None
+
+ def __init__(self, repo, source_config, eid=None):
+ super(RemoteSource, self).__init__(repo, source_config, eid)
+ self.update_config(None, self.check_conf_dict(eid, source_config,
+ fail_if_unknown=False))
+ self._query_cache = TimedCache(1800)
+
+ def update_config(self, source_entity, processed_config):
+ """update configuration from source entity"""
+ baseurl = processed_config.get('base-url')
+ if baseurl and not baseurl.endswith('/'):
+ processed_config['base-url'] += '/'
+ self.config = processed_config
+ self._skip_externals = processed_config['skip-external-entities']
+ if source_entity is not None:
+ self.latest_retrieval = source_entity.latest_retrieval
+
+ def _get_connection(self):
+ """open and return a connection to the source"""
+ self.info('connecting to source %(base-url)s with user %(cubicweb-user)s',
+ self.config)
+ cnxprops = ConnectionProperties(cnxtype=self.CNX_TYPE)
+ return dbapi.connect(login=self.config['cubicweb-user'],
+ password=self.config['cubicweb-password'],
+ cnxprops=cnxprops)
+
+ def get_connection(self):
+ try:
+ return self._get_connection()
+ except ConnectionError, ex:
+ self.critical("can't get connection to source %s: %s", self.uri, ex)
+ return ConnectionWrapper()
+
+
+ def reset_caches(self):
+ """method called during test to reset potential source caches"""
+ self._query_cache = TimedCache(1800)
+
+ def init(self, activated, source_entity):
+ """method called by the repository once ready to handle request"""
+ self.load_mapping(source_entity._cw)
+ if activated:
+ interval = self.config['synchronization-interval']
+ self.repo.looping_task(interval, self.synchronize)
+ self.repo.looping_task(self._query_cache.ttl.seconds/10,
+ self._query_cache.clear_expired)
+ self.latest_retrieval = source_entity.latest_retrieval
+
+ def load_mapping(self, session=None):
+ self.support_entities = {}
+ self.support_relations = {}
+ self.dont_cross_relations = set(('owned_by', 'created_by'))
+ self.cross_relations = set()
+ assert self.eid is not None
+ self._schemacfg_idx = {}
+ self._load_mapping(session)
+
+ etype_options = set(('write',))
+ rtype_options = set(('maycross', 'dontcross', 'write',))
+
+ def _check_options(self, schemacfg, allowedoptions):
+ if schemacfg.options:
+ options = set(w.strip() for w in schemacfg.options.split(':'))
+ else:
+ options = set()
+ if options - allowedoptions:
+ options = ', '.join(sorted(options - allowedoptions))
+ msg = _('unknown option(s): %s' % options)
+ raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
+ return options
+
+ def add_schema_config(self, schemacfg, checkonly=False):
+ """added CWSourceSchemaConfig, modify mapping accordingly"""
+ try:
+ ertype = schemacfg.schema.name
+ except AttributeError:
+ msg = schemacfg._cw._("attribute/relation can't be mapped, only "
+ "entity and relation types")
+ raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
+ if schemacfg.schema.__regid__ == 'CWEType':
+ options = self._check_options(schemacfg, self.etype_options)
+ if not checkonly:
+ self.support_entities[ertype] = 'write' in options
+ else: # CWRType
+ if ertype in ('is', 'is_instance_of', 'cw_source') or ertype in VIRTUAL_RTYPES:
+ msg = schemacfg._cw._('%s relation should not be in mapped') % ertype
+ raise ValidationError(schemacfg.eid, {role_name('cw_for_schema', 'subject'): msg})
+ options = self._check_options(schemacfg, self.rtype_options)
+ if 'dontcross' in options:
+ if 'maycross' in options:
+ msg = schemacfg._("can't mix dontcross and maycross options")
+ raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
+ if 'write' in options:
+ msg = schemacfg._("can't mix dontcross and write options")
+ raise ValidationError(schemacfg.eid, {role_name('options', 'subject'): msg})
+ if not checkonly:
+ self.dont_cross_relations.add(ertype)
+ elif not checkonly:
+ self.support_relations[ertype] = 'write' in options
+ if 'maycross' in options:
+ self.cross_relations.add(ertype)
+ if not checkonly:
+ # add to an index to ease deletion handling
+ self._schemacfg_idx[schemacfg.eid] = ertype
+
+ def del_schema_config(self, schemacfg, checkonly=False):
+ """deleted CWSourceSchemaConfig, modify mapping accordingly"""
+ if checkonly:
+ return
+ try:
+ ertype = self._schemacfg_idx[schemacfg.eid]
+ if ertype[0].isupper():
+ del self.support_entities[ertype]
+ else:
+ if ertype in self.support_relations:
+ del self.support_relations[ertype]
+ if ertype in self.cross_relations:
+ self.cross_relations.remove(ertype)
+ else:
+ self.dont_cross_relations.remove(ertype)
+ except Exception:
+ self.error('while updating mapping consequently to removal of %s',
+ schemacfg)
+
+ def local_eid(self, cnx, extid, session):
+ etype, dexturi, dextid = cnx.describe(extid)
+ if dexturi == 'system' or not (
+ dexturi in self.repo.sources_by_uri or self._skip_externals):
+ assert etype in self.support_entities, etype
+ eid = self.repo.extid2eid(self, str(extid), etype, session)
+ if eid > 0:
+ return eid, True
+ elif dexturi in self.repo.sources_by_uri:
+ source = self.repo.sources_by_uri[dexturi]
+ cnx = session.cnxset.connection(source.uri)
+ eid = source.local_eid(cnx, dextid, session)[0]
+ return eid, False
+ return None, None
+
+ def synchronize(self, mtime=None):
+ """synchronize content known by this repository with content in the
+ external repository
+ """
+ self.info('synchronizing remote %s source %s', (self.CNX_TYPE, self.uri))
+ cnx = self.get_connection()
+ try:
+ extrepo = cnx._repo
+ except AttributeError:
+ # fake connection wrapper returned when we can't connect to the
+ # external source (hence we've no chance to synchronize...)
+ return
+ etypes = self.support_entities.keys()
+ if mtime is None:
+ mtime = self.latest_retrieval
+ updatetime, modified, deleted = extrepo.entities_modified_since(
+ etypes, mtime)
+ self._query_cache.clear()
+ repo = self.repo
+ session = repo.internal_session()
+ source = repo.system_source
+ try:
+ for etype, extid in modified:
+ try:
+ eid = self.local_eid(cnx, extid, session)[0]
+ if eid is not None:
+ rset = session.eid_rset(eid, etype)
+ entity = rset.get_entity(0, 0)
+ entity.complete(entity.e_schema.indexable_attributes())
+ source.index_entity(session, entity)
+ except Exception:
+ self.exception('while updating %s with external id %s of source %s',
+ etype, extid, self.uri)
+ continue
+ for etype, extid in deleted:
+ try:
+ eid = self.repo.extid2eid(self, str(extid), etype, session,
+ insert=False)
+ # entity has been deleted from external repository but is not known here
+ if eid is not None:
+ entity = session.entity_from_eid(eid, etype)
+ repo.delete_info(session, entity, self.uri,
+ scleanup=self.eid)
+ except Exception:
+ if self.repo.config.mode == 'test':
+ raise
+ self.exception('while updating %s with external id %s of source %s',
+ etype, extid, self.uri)
+ continue
+ self.latest_retrieval = updatetime
+ session.execute('SET X latest_retrieval %(date)s WHERE X eid %(x)s',
+ {'x': self.eid, 'date': self.latest_retrieval})
+ session.commit()
+ finally:
+ session.close()
+
+ def get_connection(self):
+ raise NotImplementedError()
+
+ def check_connection(self, cnx):
+ """check connection validity, return None if the connection is still valid
+ else a new connection
+ """
+ if not isinstance(cnx, ConnectionWrapper):
+ try:
+ cnx.check()
+ return # ok
+ except BadConnectionId:
+ pass
+ # try to reconnect
+ return self.get_connection()
+
+ def syntax_tree_search(self, session, union, args=None, cachekey=None,
+ varmap=None):
+ assert dbg_st_search(self.uri, union, varmap, args, cachekey)
+ rqlkey = union.as_string(kwargs=args)
+ try:
+ results = self._query_cache[rqlkey]
+ except KeyError:
+ results = self._syntax_tree_search(session, union, args)
+ self._query_cache[rqlkey] = results
+ assert dbg_results(results)
+ return results
+
+ def _syntax_tree_search(self, session, union, args):
+ """return result from this source for a rql query (actually from a rql
+ syntax tree and a solution dictionary mapping each used variable to a
+ possible type). If cachekey is given, the query necessary to fetch the
+ results (but not the results themselves) may be cached using this key.
+ """
+ if not args is None:
+ args = args.copy()
+ # get cached cursor anyway
+ cu = session.cnxset[self.uri]
+ if cu is None:
+ # this is a ConnectionWrapper instance
+ msg = session._("can't connect to source %s, some data may be missing")
+ session.set_shared_data('sources_error', msg % self.uri, txdata=True)
+ return []
+ translator = RQL2RQL(self)
+ try:
+ rql = translator.generate(session, union, args)
+ except UnknownEid, ex:
+ if server.DEBUG:
+ print ' unknown eid', ex, 'no results'
+ return []
+ if server.DEBUG & server.DBG_RQL:
+ print ' translated rql', rql
+ try:
+ rset = cu.execute(rql, args)
+ except Exception, ex:
+ self.exception(str(ex))
+ msg = session._("error while querying source %s, some data may be missing")
+ session.set_shared_data('sources_error', msg % self.uri, txdata=True)
+ return []
+ descr = rset.description
+ if rset:
+ needtranslation = []
+ rows = rset.rows
+ for i, etype in enumerate(descr[0]):
+ if (etype is None or not self.schema.eschema(etype).final
+ or uidtype(union, i, etype, args)):
+ needtranslation.append(i)
+ if needtranslation:
+ cnx = session.cnxset.connection(self.uri)
+ for rowindex in xrange(rset.rowcount - 1, -1, -1):
+ row = rows[rowindex]
+ localrow = False
+ for colindex in needtranslation:
+ if row[colindex] is not None: # optional variable
+ eid, local = self.local_eid(cnx, row[colindex], session)
+ if local:
+ localrow = True
+ if eid is not None:
+ row[colindex] = eid
+ else:
+ # skip this row
+ del rows[rowindex]
+ del descr[rowindex]
+ break
+ else:
+ # skip row if it only contains eids of entities which
+ # are actually from a source we also know locally,
+ # except if some args specified (XXX should actually
+ # check if there are some args local to the source)
+ if not (translator.has_local_eid or localrow):
+ del rows[rowindex]
+ del descr[rowindex]
+ results = rows
+ else:
+ results = []
+ return results
+
+ def _entity_relations_and_kwargs(self, session, entity):
+ relations = []
+ kwargs = {'x': self.repo.eid2extid(self, entity.eid, session)}
+ for key, val in entity.cw_attr_cache.iteritems():
+ relations.append('X %s %%(%s)s' % (key, key))
+ kwargs[key] = val
+ return relations, kwargs
+
+ def add_entity(self, session, entity):
+ """add a new entity to the source"""
+ raise NotImplementedError()
+
+ def update_entity(self, session, entity):
+ """update an entity in the source"""
+ relations, kwargs = self._entity_relations_and_kwargs(session, entity)
+ cu = session.cnxset[self.uri]
+ cu.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs)
+ self._query_cache.clear()
+ entity.cw_clear_all_caches()
+
+ def delete_entity(self, session, entity):
+ """delete an entity from the source"""
+ if session.deleted_in_transaction(self.eid):
+ # source is being deleted, don't propagate
+ self._query_cache.clear()
+ return
+ cu = session.cnxset[self.uri]
+ cu.execute('DELETE %s X WHERE X eid %%(x)s' % entity.__regid__,
+ {'x': self.repo.eid2extid(self, entity.eid, session)})
+ self._query_cache.clear()
+
+ def add_relation(self, session, subject, rtype, object):
+ """add a relation to the source"""
+ cu = session.cnxset[self.uri]
+ cu.execute('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
+ {'x': self.repo.eid2extid(self, subject, session),
+ 'y': self.repo.eid2extid(self, object, session)})
+ self._query_cache.clear()
+ session.entity_from_eid(subject).cw_clear_all_caches()
+ session.entity_from_eid(object).cw_clear_all_caches()
+
+ def delete_relation(self, session, subject, rtype, object):
+ """delete a relation from the source"""
+ if session.deleted_in_transaction(self.eid):
+ # source is being deleted, don't propagate
+ self._query_cache.clear()
+ return
+ cu = session.cnxset[self.uri]
+ cu.execute('DELETE X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
+ {'x': self.repo.eid2extid(self, subject, session),
+ 'y': self.repo.eid2extid(self, object, session)})
+ self._query_cache.clear()
+ session.entity_from_eid(subject).cw_clear_all_caches()
+ session.entity_from_eid(object).cw_clear_all_caches()
+
+
+class RQL2RQL(object):
+ """translate a local rql query to be executed on a distant repository"""
+ def __init__(self, source):
+ self.source = source
+ self.repo = source.repo
+ self.current_operator = None
+
+ def _accept_children(self, node):
+ res = []
+ for child in node.children:
+ rql = child.accept(self)
+ if rql is not None:
+ res.append(rql)
+ return res
+
+ def generate(self, session, rqlst, args):
+ self._session = session
+ self.kwargs = args
+ self.need_translation = False
+ self.has_local_eid = False
+ return self.visit_union(rqlst)
+
+ def visit_union(self, node):
+ s = self._accept_children(node)
+ if len(s) > 1:
+ return ' UNION '.join('(%s)' % q for q in s)
+ return s[0]
+
+ def visit_select(self, node):
+ """return the tree as an encoded rql string"""
+ self._varmaker = rqlvar_maker(defined=node.defined_vars.copy())
+ self._const_var = {}
+ if node.distinct:
+ base = 'DISTINCT Any'
+ else:
+ base = 'Any'
+ s = ['%s %s' % (base, ','.join(v.accept(self) for v in node.selection))]
+ if node.groupby:
+ s.append('GROUPBY %s' % ', '.join(group.accept(self)
+ for group in node.groupby))
+ if node.orderby:
+ s.append('ORDERBY %s' % ', '.join(self.visit_sortterm(term)
+ for term in node.orderby))
+ if node.limit is not None:
+ s.append('LIMIT %s' % node.limit)
+ if node.offset:
+ s.append('OFFSET %s' % node.offset)
+ restrictions = []
+ if node.where is not None:
+ nr = node.where.accept(self)
+ if nr is not None:
+ restrictions.append(nr)
+ if restrictions:
+ s.append('WHERE %s' % ','.join(restrictions))
+
+ if node.having:
+ s.append('HAVING %s' % ', '.join(term.accept(self)
+ for term in node.having))
+ subqueries = []
+ for subquery in node.with_:
+ subqueries.append('%s BEING (%s)' % (','.join(ca.name for ca in subquery.aliases),
+ self.visit_union(subquery.query)))
+ if subqueries:
+ s.append('WITH %s' % (','.join(subqueries)))
+ return ' '.join(s)
+
+ def visit_and(self, node):
+ res = self._accept_children(node)
+ if res:
+ return ', '.join(res)
+ return
+
+ def visit_or(self, node):
+ res = self._accept_children(node)
+ if len(res) > 1:
+ return ' OR '.join('(%s)' % rql for rql in res)
+ elif res:
+ return res[0]
+ return
+
+ def visit_not(self, node):
+ rql = node.children[0].accept(self)
+ if rql:
+ return 'NOT (%s)' % rql
+ return
+
+ def visit_exists(self, node):
+ rql = node.children[0].accept(self)
+ if rql:
+ return 'EXISTS(%s)' % rql
+ return
+
+ def visit_relation(self, node):
+ try:
+ if isinstance(node.children[0], Constant):
+ # simplified rqlst, reintroduce eid relation
+ try:
+ restr, lhs = self.process_eid_const(node.children[0])
+ except UnknownEid:
+ # can safely skip not relation with an unsupported eid
+ if neged_relation(node):
+ return
+ raise
+ else:
+ lhs = node.children[0].accept(self)
+ restr = None
+ except UnknownEid:
+ # can safely skip not relation with an unsupported eid
+ if neged_relation(node):
+ return
+ # XXX what about optional relation or outer NOT EXISTS()
+ raise
+ if node.optional in ('left', 'both'):
+ lhs += '?'
+ if node.r_type == 'eid' or not self.source.schema.rschema(node.r_type).final:
+ self.need_translation = True
+ self.current_operator = node.operator()
+ if isinstance(node.children[0], Constant):
+ self.current_etypes = (node.children[0].uidtype,)
+ else:
+ self.current_etypes = node.children[0].variable.stinfo['possibletypes']
+ try:
+ rhs = node.children[1].accept(self)
+ except UnknownEid:
+ # can safely skip not relation with an unsupported eid
+ if neged_relation(node):
+ return
+ # XXX what about optional relation or outer NOT EXISTS()
+ raise
+ except ReplaceByInOperator, ex:
+ rhs = 'IN (%s)' % ','.join(eid for eid in ex.eids)
+ self.need_translation = False
+ self.current_operator = None
+ if node.optional in ('right', 'both'):
+ rhs += '?'
+ if restr is not None:
+ return '%s %s %s, %s' % (lhs, node.r_type, rhs, restr)
+ return '%s %s %s' % (lhs, node.r_type, rhs)
+
+ def visit_comparison(self, node):
+ if node.operator in ('=', 'IS'):
+ return node.children[0].accept(self)
+ return '%s %s' % (node.operator.encode(),
+ node.children[0].accept(self))
+
+ def visit_mathexpression(self, node):
+ return '(%s %s %s)' % (node.children[0].accept(self),
+ node.operator.encode(),
+ node.children[1].accept(self))
+
+ def visit_function(self, node):
+ #if node.name == 'IN':
+ res = []
+ for child in node.children:
+ try:
+ rql = child.accept(self)
+ except UnknownEid, ex:
+ continue
+ res.append(rql)
+ if not res:
+ raise ex
+ return '%s(%s)' % (node.name, ', '.join(res))
+
+ def visit_constant(self, node):
+ if self.need_translation or node.uidtype:
+ if node.type == 'Int':
+ self.has_local_eid = True
+ return str(self.eid2extid(node.value))
+ if node.type == 'Substitute':
+ key = node.value
+ # ensure we have not yet translated the value...
+ if not key in self._const_var:
+ self.kwargs[key] = self.eid2extid(self.kwargs[key])
+ self._const_var[key] = None
+ self.has_local_eid = True
+ return node.as_string()
+
+ def visit_variableref(self, node):
+ """get the sql name for a variable reference"""
+ return node.name
+
+ def visit_sortterm(self, node):
+ if node.asc:
+ return node.term.accept(self)
+ return '%s DESC' % node.term.accept(self)
+
+ def process_eid_const(self, const):
+ value = const.eval(self.kwargs)
+ try:
+ return None, self._const_var[value]
+ except Exception:
+ var = self._varmaker.next()
+ self.need_translation = True
+ restr = '%s eid %s' % (var, self.visit_constant(const))
+ self.need_translation = False
+ self._const_var[value] = var
+ return restr, var
+
+ def eid2extid(self, eid):
+ try:
+ return self.repo.eid2extid(self.source, eid, self._session)
+ except UnknownEid:
+ operator = self.current_operator
+ if operator is not None and operator != '=':
+ # deal with query like "X eid > 12"
+ #
+ # The problem is that eid order in the external source may
+ # differ from the local source
+ #
+ # So search for all eids from this source matching the condition
+ # locally and then to replace the "> 12" branch by "IN (eids)"
+ #
+ # XXX we may have to insert a huge number of eids...)
+ sql = "SELECT extid FROM entities WHERE source='%s' AND type IN (%s) AND eid%s%s"
+ etypes = ','.join("'%s'" % etype for etype in self.current_etypes)
+ cu = self._session.system_sql(sql % (self.source.uri, etypes,
+ operator, eid))
+ # XXX buggy cu.rowcount which may be zero while there are some
+ # results
+ rows = cu.fetchall()
+ if rows:
+ raise ReplaceByInOperator((b64decode(r[0]) for r in rows))
+ raise
+
--- a/server/sources/rql2sql.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/rql2sql.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -1284,10 +1284,10 @@
def _visit_var_attr_relation(self, relation, rhs_vars):
"""visit an attribute relation with variable(s) in the RHS
- attribute variables are used either in the selection or for
- unification (eg X attr1 A, Y attr2 A). In case of selection,
- nothing to do here.
+ attribute variables are used either in the selection or for unification
+ (eg X attr1 A, Y attr2 A). In case of selection, nothing to do here.
"""
+ ored = relation.ored()
for vref in rhs_vars:
var = vref.variable
if var.name in self._varmap:
@@ -1298,10 +1298,21 @@
principal = 1
else:
principal = var.stinfo.get('principal')
- if principal is not None and principal is not relation:
+ # we've to return some sql if:
+ # 1. visited relation is ored
+ # 2. variable's principal is not this relation and not 1.
+ if ored or (principal is not None and principal is not relation
+ and not getattr(principal, 'ored', lambda : 0)()):
# we have to generate unification expression
- lhssql = self._inlined_var_sql(relation.children[0].variable,
- relation.r_type)
+ if principal is relation:
+ # take care if ored case and principal is the relation to
+ # use the right relation in the unification term
+ _rel = [rel for rel in var.stinfo['rhsrelations']
+ if not rel is principal][0]
+ else:
+ _rel = relation
+ lhssql = self._inlined_var_sql(_rel.children[0].variable,
+ _rel.r_type)
try:
self._state.ignore_varmap = True
sql = lhssql + relation.children[1].accept(self)
--- a/server/sources/storages.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/sources/storages.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -194,16 +194,16 @@
# Mark the new file as added during the transaction.
# The file will be removed on rollback
AddFileOp.get_instance(entity._cw).add_data(fpath)
- if oldpath != fpath:
- # register the new location for the file.
+ # reinstall poped value
if fpath is None:
entity.cw_edited.edited_attribute(attr, None)
else:
+ # register the new location for the file.
entity.cw_edited.edited_attribute(attr, Binary(fpath))
+ if oldpath is not None and oldpath != fpath:
# Mark the old file as useless so the file will be removed at
# commit.
- if oldpath is not None:
- DeleteFileOp.get_instance(entity._cw).add_data(oldpath)
+ DeleteFileOp.get_instance(entity._cw).add_data(oldpath)
return binary
def entity_deleted(self, entity, attr):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/sources/zmqrql.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,27 @@
+# copyright 2012 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/>.
+"""Source to query another RQL repository using pyro"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from cubicweb.server.sources.remoterql import RemoteSource
+
+class ZMQRQLSource(RemoteSource):
+ """External repository source, using ZMQ sockets"""
+ CNX_TYPE = 'zmq'
--- a/server/ssplanner.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/ssplanner.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -27,7 +27,6 @@
from cubicweb import QueryError, typed_eid
from cubicweb.schema import VIRTUAL_RTYPES
from cubicweb.rqlrewrite import add_types_restriction
-from cubicweb.server.session import security_enabled
from cubicweb.server.edition import EditedEntity
READ_ONLY_RTYPES = set(('eid', 'has_text', 'is', 'is_instance_of', 'identity'))
@@ -87,7 +86,7 @@
# the generated select substep if not emited (eg nothing
# to be selected)
if checkread and eid not in neweids:
- with security_enabled(session, read=False):
+ with session.security_enabled(read=False):
eschema(session.describe(eid)[0]).check_perm(
session, 'read', eid=eid)
eidconsts[lhs.variable] = eid
--- a/server/test/data/slapd.conf.in Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/data/slapd.conf.in Tue Oct 23 15:00:53 2012 +0200
@@ -45,9 +45,9 @@
suffix "dc=cubicweb,dc=test"
# rootdn directive for specifying a superuser on the database. This is needed
-# for syncrepl.
-#rootdn "cn=admin,dc=cubicweb,dc=test"
-#rootpw "cubicwebrocks"
+# for syncrepl. and ldapdelete easyness
+rootdn "cn=admin,dc=cubicweb,dc=test"
+rootpw "cw"
# Where the database file are physically stored for database #1
directory "%(apphome)s/ldapdb"
--- a/server/test/unittest_checkintegrity.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_checkintegrity.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/server/test/unittest_datafeed.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_datafeed.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2011-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -54,7 +54,7 @@
stats = dfsource.pull_data(session, force=True)
self.commit()
# test import stats
- self.assertEqual(sorted(stats.keys()), ['created', 'updated'])
+ self.assertEqual(sorted(stats.keys()), ['checked', 'created', 'updated'])
self.assertEqual(len(stats['created']), 1)
entity = self.execute('Card X').get_entity(0, 0)
self.assertIn(entity.eid, stats['created'])
--- a/server/test/unittest_hook.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_hook.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/server/test/unittest_ldapuser.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_ldapuser.py Tue Oct 23 15:00:53 2012 +0200
@@ -16,13 +16,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/>.
"""cubicweb.server.sources.ldapusers unit and functional tests"""
+from __future__ import with_statement
import os
import shutil
import time
-from os.path import abspath, join, exists
+from os.path import join, exists
import subprocess
-from socket import socket, error as socketerror
from logilab.common.testlib import TestCase, unittest_main, mock_object, Tags
@@ -32,72 +32,70 @@
from cubicweb.devtools.httptest import get_available_port
from cubicweb.devtools import get_test_db_handler
-from cubicweb.server.sources.ldapuser import *
+from cubicweb.server.sources.ldapuser import GlobTrFunc, UnknownEid, RQL2LDAPFilter
-CONFIG = u'''user-base-dn=ou=People,dc=cubicweb,dc=test
-user-scope=ONELEVEL
-user-classes=top,posixAccount
-user-login-attr=uid
-user-default-group=users
-user-attrs-map=gecos:email,uid:login
-'''
+CONFIG = u'user-base-dn=ou=People,dc=cubicweb,dc=test'
URL = None
-def setUpModule(*args):
- create_slapd_configuration(LDAPUserSourceTC.config)
-
-def tearDownModule(*args):
- terminate_slapd()
-
-def create_slapd_configuration(config):
- global slapd_process, URL
+def create_slapd_configuration(cls):
+ global URL
+ config = cls.config
basedir = join(config.apphome, "ldapdb")
slapdconf = join(config.apphome, "slapd.conf")
confin = file(join(config.apphome, "slapd.conf.in")).read()
confstream = file(slapdconf, 'w')
confstream.write(confin % {'apphome': config.apphome})
confstream.close()
- if not exists(basedir):
- os.makedirs(basedir)
- # fill ldap server with some data
- ldiffile = join(config.apphome, "ldap_test.ldif")
- print "Initing ldap database"
- cmdline = "/usr/sbin/slapadd -f %s -l %s -c" % (slapdconf, ldiffile)
- subprocess.call(cmdline, shell=True)
-
+ if exists(basedir):
+ shutil.rmtree(basedir)
+ os.makedirs(basedir)
+ # fill ldap server with some data
+ ldiffile = join(config.apphome, "ldap_test.ldif")
+ config.info('Initing ldap database')
+ cmdline = "/usr/sbin/slapadd -f %s -l %s -c" % (slapdconf, ldiffile)
+ subprocess.call(cmdline, shell=True)
#ldapuri = 'ldapi://' + join(basedir, "ldapi").replace('/', '%2f')
port = get_available_port(xrange(9000, 9100))
host = 'localhost:%s' % port
ldapuri = 'ldap://%s' % host
cmdline = ["/usr/sbin/slapd", "-f", slapdconf, "-h", ldapuri, "-d", "0"]
- print 'Starting slapd:', ' '.join(cmdline)
- slapd_process = subprocess.Popen(cmdline)
+ config.info('Starting slapd:', ' '.join(cmdline))
+ cls.slapd_process = subprocess.Popen(cmdline)
time.sleep(0.2)
- if slapd_process.poll() is None:
- print "slapd started with pid %s" % slapd_process.pid
+ if cls.slapd_process.poll() is None:
+ config.info('slapd started with pid %s' % cls.slapd_process.pid)
else:
raise EnvironmentError('Cannot start slapd with cmdline="%s" (from directory "%s")' %
(" ".join(cmdline), os.getcwd()))
URL = u'ldap://%s' % host
-def terminate_slapd():
- global slapd_process
- if slapd_process.returncode is None:
- print "terminating slapd"
- if hasattr(slapd_process, 'terminate'):
- slapd_process.terminate()
+def terminate_slapd(cls):
+ config = cls.config
+ if cls.slapd_process and cls.slapd_process.returncode is None:
+ config.info('terminating slapd')
+ if hasattr(cls.slapd_process, 'terminate'):
+ cls.slapd_process.terminate()
else:
import os, signal
- os.kill(slapd_process.pid, signal.SIGTERM)
- slapd_process.wait()
- print "DONE"
- del slapd_process
+ os.kill(cls.slapd_process.pid, signal.SIGTERM)
+ cls.slapd_process.wait()
+ config.info('DONE')
+
+class LDAPTestBase(CubicWebTC):
+ loglevel = 'ERROR'
-
+ @classmethod
+ def setUpClass(cls):
+ from cubicweb.cwctl import init_cmdline_log_threshold
+ init_cmdline_log_threshold(cls.config, cls.loglevel)
+ create_slapd_configuration(cls)
+ @classmethod
+ def tearDownClass(cls):
+ terminate_slapd(cls)
-class LDAPFeedSourceTC(CubicWebTC):
+class DeleteStuffFromLDAPFeedSourceTC(LDAPTestBase):
test_db_id = 'ldap-feed'
@classmethod
@@ -105,7 +103,57 @@
session.create_entity('CWSource', name=u'ldapuser', type=u'ldapfeed', parser=u'ldapfeed',
url=URL, config=CONFIG)
session.commit()
- isession = session.repo.internal_session()
+ isession = session.repo.internal_session(safe=True)
+ lfsource = isession.repo.sources_by_uri['ldapuser']
+ stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
+
+ def _pull(self):
+ with self.session.repo.internal_session() as isession:
+ lfsource = isession.repo.sources_by_uri['ldapuser']
+ stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
+ isession.commit()
+
+ def test_delete(self):
+ """ delete syt, pull, check deactivation, repull,
+ readd syt, pull, check activation
+ """
+ uri = self.repo.sources_by_uri['ldapuser'].urls[0]
+ deletecmd = ("ldapdelete -H %s 'uid=syt,ou=People,dc=cubicweb,dc=test' "
+ "-v -x -D cn=admin,dc=cubicweb,dc=test -w'cw'" % uri)
+ os.system(deletecmd)
+ self._pull()
+ self.assertRaises(AuthenticationError, self.repo.connect, 'syt', password='syt')
+ self.assertEqual(self.execute('Any N WHERE U login "syt", '
+ 'U in_state S, S name N').rows[0][0],
+ 'deactivated')
+ # check that it doesn't choke
+ self._pull()
+ # reset the fscking ldap thing
+ self.tearDownClass()
+ self.setUpClass()
+ self._pull()
+ # still deactivated, but a warning has been emitted ...
+ self.assertEqual(self.execute('Any N WHERE U login "syt", '
+ 'U in_state S, S name N').rows[0][0],
+ 'deactivated')
+ # test reactivating the user isn't enough to authenticate, as the native source
+ # refuse to authenticate user from other sources
+ os.system(deletecmd)
+ self._pull()
+ user = self.execute('CWUser U WHERE U login "syt"').get_entity(0, 0)
+ user.cw_adapt_to('IWorkflowable').fire_transition('activate')
+ self.commit()
+ self.assertRaises(AuthenticationError, self.repo.connect, 'syt', password='syt')
+
+class LDAPFeedSourceTC(LDAPTestBase):
+ test_db_id = 'ldap-feed'
+
+ @classmethod
+ def pre_setup_database(cls, session, config):
+ session.create_entity('CWSource', name=u'ldapuser', type=u'ldapfeed', parser=u'ldapfeed',
+ url=URL, config=CONFIG)
+ session.commit()
+ isession = session.repo.internal_session(safe=True)
lfsource = isession.repo.sources_by_uri['ldapuser']
stats = lfsource.pull_data(isession, force=True, raise_on_error=True)
@@ -156,16 +204,23 @@
self.assertEqual(len(rset), 1)
e = rset.get_entity(0, 0)
self.assertEqual(e.eid, eid)
- self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system', 'use-cwuri-as-url': False},
+ self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native',
+ 'uri': u'system',
+ 'use-cwuri-as-url': False},
'type': 'CWUser',
'extid': None})
self.assertEqual(e.cw_source[0].name, 'system')
self.assertTrue(e.creation_date)
self.assertTrue(e.modification_date)
- # XXX test some password has been set
source.pull_data(self.session)
rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
self.assertEqual(len(rset), 1)
+ # test some password has been set
+ cu = self.session.system_sql('SELECT cw_upassword FROM cw_CWUser WHERE cw_eid=%s' % rset[0][0])
+ value = str(cu.fetchall()[0][0])
+ self.assertEqual(value, '{SSHA}v/8xJQP3uoaTBZz1T7Y0B3qOxRN1cj7D')
+ self.assertTrue(self.repo.system_source.authenticate(
+ self.session, 'syt', password='syt'))
class LDAPUserSourceTC(LDAPFeedSourceTC):
--- a/server/test/unittest_msplanner.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_msplanner.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/server/test/unittest_multisources.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_multisources.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -337,7 +337,7 @@
ceid = cu.execute('INSERT Card X: X title "without wikiid to get eid based url"')[0][0]
self.cnx2.commit()
lc = self.sexecute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0)
- self.assertEqual(lc.absolute_url(), 'http://extern.org/card/eid/%s' % ceid)
+ self.assertEqual(lc.absolute_url(), 'http://extern.org/%s' % ceid)
cu.execute('DELETE Card X WHERE X eid %(x)s', {'x':ceid})
self.cnx2.commit()
@@ -346,7 +346,7 @@
ceid = cu.execute('INSERT Card X: X title "without wikiid to get eid based url"')[0][0]
self.cnx3.commit()
lc = self.sexecute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0)
- self.assertEqual(lc.absolute_url(), 'http://testing.fr/cubicweb/card/eid/%s' % lc.eid)
+ self.assertEqual(lc.absolute_url(), 'http://testing.fr/cubicweb/%s' % lc.eid)
cu.execute('DELETE Card X WHERE X eid %(x)s', {'x':ceid})
self.cnx3.commit()
--- a/server/test/unittest_postgres.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_postgres.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,20 +1,20 @@
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
-# This file is part of Logilab-common.
+# This file is part of CubicWeb.
#
-# Logilab-common 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 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.
#
-# Logilab-common is distributed in the hope that it will be useful, but WITHOUT
+# 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 Logilab-common. If not, see <http://www.gnu.org/licenses/>.
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
from __future__ import with_statement
--- a/server/test/unittest_querier.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_querier.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
# -*- coding: iso-8859-1 -*-
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -18,6 +18,8 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""unit tests for modules cubicweb.server.querier and cubicweb.server.ssplanner
"""
+from __future__ import with_statement
+
from datetime import date, datetime, timedelta, tzinfo
from logilab.common.testlib import TestCase, unittest_main
@@ -27,10 +29,10 @@
from cubicweb.server.sqlutils import SQL_PREFIX
from cubicweb.server.utils import crypt_password
from cubicweb.server.sources.native import make_schema
+from cubicweb.server.querier import manual_build_descr, _make_description
from cubicweb.devtools import get_test_db_handler, TestServerConfiguration
-
+from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.devtools.repotest import tuplify, BaseQuerierTC
-from unittest_session import Variable
class FixedOffset(tzinfo):
def __init__(self, hours=0):
@@ -70,24 +72,48 @@
('C0 text,C1 integer', {'A': 'table0.C0', 'B': 'table0.C1'}))
-def setUpModule(*args):
+def setUpClass(cls, *args):
global repo, cnx
config = TestServerConfiguration(apphome=UtilsTC.datadir)
handler = get_test_db_handler(config)
handler.build_db_cache()
repo, cnx = handler.get_repo_and_cnx()
+ cls.repo = repo
-def tearDownModule(*args):
+def tearDownClass(cls, *args):
global repo, cnx
cnx.close()
repo.shutdown()
del repo, cnx
+class Variable:
+ def __init__(self, name):
+ self.name = name
+ self.children = []
+
+ def get_type(self, solution, args=None):
+ return solution[self.name]
+ def as_string(self):
+ return self.name
+
+class Function:
+ def __init__(self, name, varname):
+ self.name = name
+ self.children = [Variable(varname)]
+ def get_type(self, solution, args=None):
+ return 'Int'
+
+class MakeDescriptionTC(TestCase):
+ def test_known_values(self):
+ solution = {'A': 'Int', 'B': 'CWUser'}
+ self.assertEqual(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
+ ['Int','CWUser'])
+
+
class UtilsTC(BaseQuerierTC):
- def setUp(self):
- self.__class__.repo = repo
- super(UtilsTC, self).setUp()
+ setUpClass = classmethod(setUpClass)
+ tearDownClass = classmethod(tearDownClass)
def get_max_eid(self):
# no need for cleanup here
@@ -240,11 +266,32 @@
rset = self.execute('Any %(x)s', {'x': u'str'})
self.assertEqual(rset.description[0][0], 'String')
+ def test_build_descr1(self):
+ rset = self.execute('(Any U,L WHERE U login L) UNION (Any G,N WHERE G name N, G is CWGroup)')
+ rset.req = self.transaction
+ orig_length = len(rset)
+ rset.rows[0][0] = 9999999
+ description = manual_build_descr(rset.req, rset.syntax_tree(), None, rset.rows)
+ self.assertEqual(len(description), orig_length - 1)
+ self.assertEqual(len(rset.rows), orig_length - 1)
+ self.assertNotEqual(rset.rows[0][0], 9999999)
+
+ def test_build_descr2(self):
+ rset = self.execute('Any X,Y WITH X,Y BEING ((Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G))')
+ for x, y in rset.description:
+ if y is not None:
+ self.assertEqual(y, 'CWGroup')
+
+ def test_build_descr3(self):
+ rset = self.execute('(Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G)')
+ for x, y in rset.description:
+ if y is not None:
+ self.assertEqual(y, 'CWGroup')
+
class QuerierTC(BaseQuerierTC):
- def setUp(self):
- self.__class__.repo = repo
- super(QuerierTC, self).setUp()
+ setUpClass = classmethod(setUpClass)
+ tearDownClass = classmethod(tearDownClass)
def test_encoding_pb(self):
self.assertRaises(RQLSyntaxError, self.execute,
@@ -1259,7 +1306,7 @@
cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
% (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
passwd = str(cursor.fetchone()[0])
- self.assertEqual(passwd, crypt_password('toto', passwd[:2]))
+ self.assertEqual(passwd, crypt_password('toto', passwd))
rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
{'pwd': Binary(passwd)})
self.assertEqual(len(rset.rows), 1)
@@ -1274,7 +1321,7 @@
cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
% (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
passwd = str(cursor.fetchone()[0])
- self.assertEqual(passwd, crypt_password('tutu', passwd[:2]))
+ self.assertEqual(passwd, crypt_password('tutu', passwd))
rset = self.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
{'pwd': Binary(passwd)})
self.assertEqual(len(rset.rows), 1)
@@ -1501,5 +1548,20 @@
self.assertFalse(self.execute('Any X WHERE X is CWEType, X name %(name)s', {'name': None}))
self.assertTrue(self.execute('Any X WHERE X is CWEType, X name %(name)s', {'name': 'CWEType'}))
+
+class NonRegressionTC(CubicWebTC):
+
+ def test_has_text_security_cache_bug(self):
+ req = self.request()
+ self.create_user(req, 'user', ('users',))
+ aff1 = req.create_entity('Societe', nom=u'aff1')
+ aff2 = req.create_entity('Societe', nom=u'aff2')
+ self.commit()
+ with self.login('user', password='user'):
+ res = self.execute('Any X WHERE X has_text %(text)s', {'text': 'aff1'})
+ self.assertEqual(res.rows, [[aff1.eid]])
+ res = self.execute('Any X WHERE X has_text %(text)s', {'text': 'aff2'})
+ self.assertEqual(res.rows, [[aff2.eid]])
+
if __name__ == '__main__':
unittest_main()
--- a/server/test/unittest_repository.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_repository.py Tue Oct 23 15:00:53 2012 +0200
@@ -36,7 +36,7 @@
UnknownEid, AuthenticationError, Unauthorized, QueryError)
from cubicweb.predicates import is_instance
from cubicweb.schema import CubicWebSchema, RQLConstraint
-from cubicweb.dbapi import connect, multiple_connections_unfix
+from cubicweb.dbapi import connect, multiple_connections_unfix, ConnectionProperties
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.devtools.repotest import tuplify
from cubicweb.server import repository, hook
@@ -113,6 +113,8 @@
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)
@@ -379,6 +381,65 @@
# connect monkey patch some method by default, remove them
multiple_connections_unfix()
+
+ def test_zmq(self):
+ try:
+ import zmq
+ except ImportError:
+ self.skipTest("zmq in not available")
+ done = []
+ from cubicweb.devtools import TestServerConfiguration as ServerConfiguration
+ from cubicweb.server.cwzmq import ZMQRepositoryServer
+ # the client part has to be in a thread due to sqlite limitations
+ t = threading.Thread(target=self._zmq_client, args=(done,))
+ t.start()
+
+ zmq_server = ZMQRepositoryServer(self.repo)
+ zmq_server.connect('tcp://127.0.0.1:41415')
+
+ t2 = threading.Thread(target=self._zmq_quit, args=(done, zmq_server,))
+ t2.start()
+
+ zmq_server.run()
+
+ t2.join(1)
+ t.join(1)
+
+ if t.isAlive():
+ self.fail('something went wrong, thread still alive')
+
+ def _zmq_quit(self, done, srv):
+ while not done:
+ time.sleep(0.1)
+ srv.quit()
+
+ def _zmq_client(self, done):
+ cnxprops = ConnectionProperties('zmq')
+ try:
+ cnx = connect('tcp://127.0.0.1:41415', u'admin', password=u'gingkow',
+ cnxprops=cnxprops,
+ initlog=False) # don't reset logging configuration
+ try:
+ cnx.load_appobjects(subpath=('entities',))
+ # check we can get the schema
+ schema = cnx.get_schema()
+ self.assertTrue(cnx.vreg)
+ self.assertTrue('etypes'in cnx.vreg)
+ cu = cnx.cursor()
+ rset = cu.execute('Any U,G WHERE U in_group G')
+ user = iter(rset.entities()).next()
+ self.assertTrue(user._cw)
+ self.assertTrue(user._cw.vreg)
+ from cubicweb.entities import authobjs
+ self.assertIsInstance(user._cw.user, authobjs.CWUser)
+ cnx.close()
+ done.append(True)
+ finally:
+ # connect monkey patch some method by default, remove them
+ multiple_connections_unfix()
+ finally:
+ done.append(False)
+
def test_internal_api(self):
repo = self.repo
cnxid = repo.connect(self.admlogin, password=self.admpassword)
@@ -461,7 +522,7 @@
self.commit()
self.assertEqual(len(c.reverse_fiche), 1)
- def test_set_attributes_in_before_update(self):
+ def test_cw_set_in_before_update(self):
# local hook
class DummyBeforeHook(Hook):
__regid__ = 'dummy-before-hook'
@@ -473,31 +534,31 @@
pendings = self._cw.transaction_data.setdefault('pending', set())
if self.entity.eid not in pendings:
pendings.add(self.entity.eid)
- self.entity.set_attributes(alias=u'foo')
+ self.entity.cw_set(alias=u'foo')
with self.temporary_appobjects(DummyBeforeHook):
req = self.request()
addr = req.create_entity('EmailAddress', address=u'a@b.fr')
- addr.set_attributes(address=u'a@b.com')
+ addr.cw_set(address=u'a@b.com')
rset = self.execute('Any A,AA WHERE X eid %(x)s, X address A, X alias AA',
{'x': addr.eid})
self.assertEqual(rset.rows, [[u'a@b.com', u'foo']])
- def test_set_attributes_in_before_add(self):
+ def test_cw_set_in_before_add(self):
# local hook
class DummyBeforeHook(Hook):
__regid__ = 'dummy-before-hook'
__select__ = Hook.__select__ & is_instance('EmailAddress')
events = ('before_add_entity',)
def __call__(self):
- # set_attributes is forbidden within before_add_entity()
- self.entity.set_attributes(alias=u'foo')
+ # cw_set is forbidden within before_add_entity()
+ self.entity.cw_set(alias=u'foo')
with self.temporary_appobjects(DummyBeforeHook):
req = self.request()
# XXX will fail with python -O
self.assertRaises(AssertionError, req.create_entity,
'EmailAddress', address=u'a@b.fr')
- def test_multiple_edit_set_attributes(self):
+ def test_multiple_edit_cw_set(self):
"""make sure cw_edited doesn't get cluttered
by previous entities on multiple set
"""
@@ -603,7 +664,7 @@
self.commit()
rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
self.assertEqual(rset.rows, [])
- req.user.set_relations(use_email=toto)
+ req.user.cw_set(use_email=toto)
self.commit()
rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
self.assertEqual(rset.rows, [[req.user.eid]])
@@ -613,11 +674,11 @@
rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'toto'})
self.assertEqual(rset.rows, [])
tutu = req.create_entity('EmailAddress', address=u'tutu@logilab.fr')
- req.user.set_relations(use_email=tutu)
+ req.user.cw_set(use_email=tutu)
self.commit()
rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'})
self.assertEqual(rset.rows, [[req.user.eid]])
- tutu.set_attributes(address=u'hip@logilab.fr')
+ tutu.cw_set(address=u'hip@logilab.fr')
self.commit()
rset = req.execute('Any X WHERE X has_text %(t)s', {'t': 'tutu'})
self.assertEqual(rset.rows, [])
@@ -729,7 +790,7 @@
personnes.append(p)
abraham = req.create_entity('Personne', nom=u'Abraham', prenom=u'John', sexe=u'M')
for j in xrange(0, 2000, 100):
- abraham.set_relations(personne_composite=personnes[j:j+100])
+ abraham.cw_set(personne_composite=personnes[j:j+100])
t1 = time.time()
self.info('creation: %.2gs', (t1 - t0))
req.cnx.commit()
@@ -755,7 +816,7 @@
t1 = time.time()
self.info('creation: %.2gs', (t1 - t0))
for j in xrange(100, 2000, 100):
- abraham.set_relations(personne_composite=personnes[j:j+100])
+ abraham.cw_set(personne_composite=personnes[j:j+100])
t2 = time.time()
self.info('more relations: %.2gs', (t2-t1))
req.cnx.commit()
@@ -775,7 +836,7 @@
t1 = time.time()
self.info('creation: %.2gs', (t1 - t0))
for j in xrange(100, 2000, 100):
- abraham.set_relations(personne_inlined=personnes[j:j+100])
+ abraham.cw_set(personne_inlined=personnes[j:j+100])
t2 = time.time()
self.info('more relations: %.2gs', (t2-t1))
req.cnx.commit()
@@ -856,7 +917,7 @@
p1 = req.create_entity('Personne', nom=u'Vincent')
p2 = req.create_entity('Personne', nom=u'Florent')
w = req.create_entity('Affaire', ref=u'wc')
- w.set_relations(todo_by=[p1,p2])
+ w.cw_set(todo_by=[p1,p2])
w.cw_clear_all_caches()
self.commit()
self.assertEqual(len(w.todo_by), 1)
@@ -867,9 +928,9 @@
p1 = req.create_entity('Personne', nom=u'Vincent')
p2 = req.create_entity('Personne', nom=u'Florent')
w = req.create_entity('Affaire', ref=u'wc')
- w.set_relations(todo_by=p1)
+ w.cw_set(todo_by=p1)
self.commit()
- w.set_relations(todo_by=p2)
+ w.cw_set(todo_by=p2)
w.cw_clear_all_caches()
self.commit()
self.assertEqual(len(w.todo_by), 1)
--- a/server/test/unittest_rql2sql.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_rql2sql.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -219,6 +219,11 @@
ADVANCED = [
+ ("Societe S WHERE S2 is Societe, S2 nom SN, S nom 'Logilab' OR S nom SN",
+ '''SELECT _S.cw_eid
+FROM cw_Societe AS _S, cw_Societe AS _S2
+WHERE ((_S.cw_nom=Logilab) OR (_S2.cw_nom=_S.cw_nom))'''),
+
("Societe S WHERE S nom 'Logilab' OR S nom 'Caesium'",
'''SELECT _S.cw_eid
FROM cw_Societe AS _S
--- a/server/test/unittest_rqlannotation.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_rqlannotation.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
# -*- coding: iso-8859-1 -*-
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -350,6 +350,12 @@
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):
+ rqlst = self._prepare('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)
+
if __name__ == '__main__':
from logilab.common.testlib import unittest_main
unittest_main()
--- a/server/test/unittest_security.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_security.py Tue Oct 23 15:00:53 2012 +0200
@@ -16,54 +16,63 @@
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""functional tests for server'security"""
+from __future__ import with_statement
import sys
from logilab.common.testlib import unittest_main, TestCase
-from cubicweb.devtools.testlib import CubicWebTC
+
+from rql import RQLException
-from cubicweb import Unauthorized, ValidationError, QueryError
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb import Unauthorized, ValidationError, QueryError, Binary
+from cubicweb.schema import ERQLExpression
from cubicweb.server.querier import check_read_access
+from cubicweb.server.utils import _CRYPTO_CTX
+
class BaseSecurityTC(CubicWebTC):
def setup_database(self):
super(BaseSecurityTC, self).setup_database()
- req = self.request()
- self.create_user(req, 'iaminusersgrouponly')
- readoriggroups = self.schema['Personne'].permissions['read']
- addoriggroups = self.schema['Personne'].permissions['add']
- def fix_perm():
- self.schema['Personne'].set_action_permissions('read', readoriggroups)
- self.schema['Personne'].set_action_permissions('add', addoriggroups)
- self.addCleanup(fix_perm)
-
+ self.create_user(self.request(), 'iaminusersgrouponly')
+ hash = _CRYPTO_CTX.encrypt('oldpassword', scheme='des_crypt')
+ self.create_user(self.request(), 'oldpassword', password=Binary(hash))
class LowLevelSecurityFunctionTC(BaseSecurityTC):
def test_check_read_access(self):
rql = u'Personne U where U nom "managers"'
rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
- origgroups = self.schema['Personne'].get_groups('read')
- self.schema['Personne'].set_action_permissions('read', ('users', 'managers'))
- self.repo.vreg.solutions(self.session, rqlst, None)
- solution = rqlst.solutions[0]
- check_read_access(self.session, rqlst, solution, {})
- cnx = self.login('anon')
- cu = cnx.cursor()
- self.assertRaises(Unauthorized,
- check_read_access,
- self.session, rqlst, solution, {})
- self.assertRaises(Unauthorized, cu.execute, rql)
+ with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
+ self.repo.vreg.solutions(self.session, rqlst, None)
+ solution = rqlst.solutions[0]
+ check_read_access(self.session, rqlst, solution, {})
+ with self.login('anon') as cu:
+ self.assertRaises(Unauthorized,
+ check_read_access,
+ self.session, rqlst, solution, {})
+ self.assertRaises(Unauthorized, cu.execute, rql)
def test_upassword_not_selectable(self):
self.assertRaises(Unauthorized,
self.execute, 'Any X,P WHERE X is CWUser, X upassword P')
self.rollback()
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- self.assertRaises(Unauthorized,
- cu.execute, 'Any X,P WHERE X is CWUser, X upassword P')
+ with self.login('iaminusersgrouponly') as cu:
+ self.assertRaises(Unauthorized,
+ cu.execute, 'Any X,P WHERE X is CWUser, X upassword P')
+
+ def test_update_password(self):
+ """Ensure that if a user's password is stored with a deprecated hash, it will be updated on next login"""
+ oldhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])
+ with self.login('oldpassword') as cu:
+ pass
+ newhash = str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0])
+ self.assertNotEqual(oldhash, newhash)
+ self.assertTrue(newhash.startswith('$6$'))
+ with self.login('oldpassword') as cu:
+ pass
+ self.assertEqual(newhash, str(self.session.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE cw_login = 'oldpassword'").fetchone()[0]))
class SecurityRewritingTC(BaseSecurityTC):
@@ -78,15 +87,14 @@
super(SecurityRewritingTC, self).tearDown()
def test_not_relation_read_security(self):
- cnx = self.login('iaminusersgrouponly')
- self.hijack_source_execute()
- self.execute('Any U WHERE NOT A todo_by U, A is Affaire')
- self.assertEqual(self.query[0][1].as_string(),
- 'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
- self.execute('Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
- self.assertEqual(self.query[0][1].as_string(),
- 'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
- cnx.close()
+ with self.login('iaminusersgrouponly'):
+ self.hijack_source_execute()
+ self.execute('Any U WHERE NOT A todo_by U, A is Affaire')
+ self.assertEqual(self.query[0][1].as_string(),
+ 'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
+ self.execute('Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
+ self.assertEqual(self.query[0][1].as_string(),
+ 'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
class SecurityTC(BaseSecurityTC):
@@ -100,76 +108,63 @@
self.commit()
def test_insert_security(self):
- cnx = self.login('anon')
- cu = cnx.cursor()
- cu.execute("INSERT Personne X: X nom 'bidule'")
- self.assertRaises(Unauthorized, cnx.commit)
- self.assertEqual(cu.execute('Personne X').rowcount, 1)
- cnx.close()
+ with self.login('anon') as cu:
+ cu.execute("INSERT Personne X: X nom 'bidule'")
+ self.assertRaises(Unauthorized, self.commit)
+ self.assertEqual(cu.execute('Personne X').rowcount, 1)
def test_insert_rql_permission(self):
# test user can only add une affaire related to a societe he owns
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- cu.execute("INSERT Affaire X: X sujet 'cool'")
- self.assertRaises(Unauthorized, cnx.commit)
+ with self.login('iaminusersgrouponly') as cu:
+ cu.execute("INSERT Affaire X: X sujet 'cool'")
+ self.assertRaises(Unauthorized, self.commit)
# test nothing has actually been inserted
- self.restore_connection()
self.assertEqual(self.execute('Affaire X').rowcount, 1)
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- cu.execute("INSERT Affaire X: X sujet 'cool'")
- cu.execute("INSERT Societe X: X nom 'chouette'")
- cu.execute("SET A concerne S WHERE A sujet 'cool', S nom 'chouette'")
- cnx.commit()
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ cu.execute("INSERT Affaire X: X sujet 'cool'")
+ cu.execute("INSERT Societe X: X nom 'chouette'")
+ cu.execute("SET A concerne S WHERE A sujet 'cool', S nom 'chouette'")
+ self.commit()
def test_update_security_1(self):
- cnx = self.login('anon')
- cu = cnx.cursor()
- # local security check
- cu.execute( "SET X nom 'bidulechouette' WHERE X is Personne")
- self.assertRaises(Unauthorized, cnx.commit)
- self.restore_connection()
+ with self.login('anon') as cu:
+ # local security check
+ cu.execute( "SET X nom 'bidulechouette' WHERE X is Personne")
+ self.assertRaises(Unauthorized, self.commit)
self.assertEqual(self.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
def test_update_security_2(self):
- cnx = self.login('anon')
- cu = cnx.cursor()
- self.repo.schema['Personne'].set_action_permissions('read', ('users', 'managers'))
- self.repo.schema['Personne'].set_action_permissions('add', ('guests', 'users', 'managers'))
- self.assertRaises(Unauthorized, cu.execute, "SET X nom 'bidulechouette' WHERE X is Personne")
- #self.assertRaises(Unauthorized, cnx.commit)
+ with self.temporary_permissions(Personne={'read': ('users', 'managers'),
+ 'add': ('guests', 'users', 'managers')}):
+ with self.login('anon') as cu:
+ self.assertRaises(Unauthorized, cu.execute, "SET X nom 'bidulechouette' WHERE X is Personne")
+ self.rollback()
+ # self.assertRaises(Unauthorized, cnx.commit)
# test nothing has actually been inserted
- self.restore_connection()
self.assertEqual(self.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
def test_update_security_3(self):
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- cu.execute("INSERT Personne X: X nom 'biduuule'")
- cu.execute("INSERT Societe X: X nom 'looogilab'")
- cu.execute("SET X travaille S WHERE X nom 'biduuule', S nom 'looogilab'")
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ cu.execute("INSERT Personne X: X nom 'biduuule'")
+ cu.execute("INSERT Societe X: X nom 'looogilab'")
+ cu.execute("SET X travaille S WHERE X nom 'biduuule', S nom 'looogilab'")
def test_update_rql_permission(self):
self.execute("SET A concerne S WHERE A is Affaire, S is Societe")
self.commit()
# test user can only update une affaire related to a societe he owns
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- cu.execute("SET X sujet 'pascool' WHERE X is Affaire")
- # this won't actually do anything since the selection query won't return anything
- cnx.commit()
- # to actually get Unauthorized exception, try to update an entity we can read
- cu.execute("SET X nom 'toto' WHERE X is Societe")
- self.assertRaises(Unauthorized, cnx.commit)
- cu.execute("INSERT Affaire X: X sujet 'pascool'")
- cu.execute("INSERT Societe X: X nom 'chouette'")
- cu.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
- cu.execute("SET X sujet 'habahsicestcool' WHERE X sujet 'pascool'")
- cnx.commit()
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ cu.execute("SET X sujet 'pascool' WHERE X is Affaire")
+ # this won't actually do anything since the selection query won't return anything
+ self.commit()
+ # to actually get Unauthorized exception, try to update an entity we can read
+ cu.execute("SET X nom 'toto' WHERE X is Societe")
+ self.assertRaises(Unauthorized, self.commit)
+ cu.execute("INSERT Affaire X: X sujet 'pascool'")
+ cu.execute("INSERT Societe X: X nom 'chouette'")
+ cu.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
+ cu.execute("SET X sujet 'habahsicestcool' WHERE X sujet 'pascool'")
+ self.commit()
def test_delete_security(self):
# FIXME: sample below fails because we don't detect "owner" can't delete
@@ -179,251 +174,223 @@
#self.assertRaises(Unauthorized,
# self.o.execute, user, "DELETE CWUser X WHERE X login 'bidule'")
# check local security
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- self.assertRaises(Unauthorized, cu.execute, "DELETE CWGroup Y WHERE Y name 'staff'")
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ self.assertRaises(Unauthorized, cu.execute, "DELETE CWGroup Y WHERE Y name 'staff'")
+ self.rollback()
def test_delete_rql_permission(self):
self.execute("SET A concerne S WHERE A is Affaire, S is Societe")
self.commit()
# test user can only dele une affaire related to a societe he owns
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- # this won't actually do anything since the selection query won't return anything
- cu.execute("DELETE Affaire X")
- cnx.commit()
- # to actually get Unauthorized exception, try to delete an entity we can read
- self.assertRaises(Unauthorized, cu.execute, "DELETE Societe S")
- self.assertRaises(QueryError, cnx.commit) # can't commit anymore
- cnx.rollback() # required after Unauthorized
- cu.execute("INSERT Affaire X: X sujet 'pascool'")
- cu.execute("INSERT Societe X: X nom 'chouette'")
- cu.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
- cnx.commit()
+ with self.login('iaminusersgrouponly') as cu:
+ # this won't actually do anything since the selection query won't return anything
+ cu.execute("DELETE Affaire X")
+ self.commit()
+ # to actually get Unauthorized exception, try to delete an entity we can read
+ self.assertRaises(Unauthorized, cu.execute, "DELETE Societe S")
+ self.assertRaises(QueryError, self.commit) # can't commit anymore
+ self.rollback() # required after Unauthorized
+ cu.execute("INSERT Affaire X: X sujet 'pascool'")
+ cu.execute("INSERT Societe X: X nom 'chouette'")
+ cu.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
+ self.commit()
## # this one should fail since it will try to delete two affaires, one authorized
## # and the other not
## self.assertRaises(Unauthorized, cu.execute, "DELETE Affaire X")
- cu.execute("DELETE Affaire X WHERE X sujet 'pascool'")
- cnx.commit()
- cnx.close()
+ cu.execute("DELETE Affaire X WHERE X sujet 'pascool'")
+ self.commit()
def test_insert_relation_rql_permission(self):
- cnx = self.login('iaminusersgrouponly')
- session = self.session
- cu = cnx.cursor(session)
- cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
- # should raise Unauthorized since user don't own S
- # though this won't actually do anything since the selection query won't return anything
- cnx.commit()
- # to actually get Unauthorized exception, try to insert a relation were we can read both entities
- rset = cu.execute('Personne P')
- self.assertEqual(len(rset), 1)
- ent = rset.get_entity(0, 0)
- session.set_cnxset() # necessary
- self.assertRaises(Unauthorized, ent.cw_check_perm, 'update')
- self.assertRaises(Unauthorized,
- cu.execute, "SET P travaille S WHERE P is Personne, S is Societe")
- self.assertRaises(QueryError, cnx.commit) # can't commit anymore
- cnx.rollback()
- # test nothing has actually been inserted:
- self.assertEqual(cu.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe').rowcount, 0)
- cu.execute("INSERT Societe X: X nom 'chouette'")
- cu.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
- cnx.commit()
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+ # should raise Unauthorized since user don't own S though this won't
+ # actually do anything since the selection query won't return
+ # anything
+ self.commit()
+ # to actually get Unauthorized exception, try to insert a relation
+ # were we can read both entities
+ rset = cu.execute('Personne P')
+ self.assertEqual(len(rset), 1)
+ ent = rset.get_entity(0, 0)
+ self.assertFalse(cu.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe'))
+ self.assertRaises(Unauthorized, ent.cw_check_perm, 'update')
+ self.assertRaises(Unauthorized,
+ cu.execute, "SET P travaille S WHERE P is Personne, S is Societe")
+ self.assertRaises(QueryError, self.commit) # can't commit anymore
+ self.rollback()
+ # test nothing has actually been inserted:
+ self.assertFalse(cu.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe'))
+ cu.execute("INSERT Societe X: X nom 'chouette'")
+ cu.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
+ self.commit()
def test_delete_relation_rql_permission(self):
self.execute("SET A concerne S WHERE A is Affaire, S is Societe")
self.commit()
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- # this won't actually do anything since the selection query won't return anything
- cu.execute("DELETE A concerne S")
- cnx.commit()
+ with self.login('iaminusersgrouponly') as cu:
+ # this won't actually do anything since the selection query won't return anything
+ cu.execute("DELETE A concerne S")
+ self.commit()
# to actually get Unauthorized exception, try to delete a relation we can read
- self.restore_connection()
eid = self.execute("INSERT Affaire X: X sujet 'pascool'")[0][0]
self.execute('SET X owned_by U WHERE X eid %(x)s, U login "iaminusersgrouponly"', {'x': eid})
self.execute("SET A concerne S WHERE A sujet 'pascool', S is Societe")
self.commit()
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- self.assertRaises(Unauthorized, cu.execute, "DELETE A concerne S")
- self.assertRaises(QueryError, cnx.commit) # can't commit anymore
- cnx.rollback() # required after Unauthorized
- cu.execute("INSERT Societe X: X nom 'chouette'")
- cu.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
- cnx.commit()
- cu.execute("DELETE A concerne S WHERE S nom 'chouette'")
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ self.assertRaises(Unauthorized, cu.execute, "DELETE A concerne S")
+ self.assertRaises(QueryError, self.commit) # can't commit anymore
+ self.rollback() # required after Unauthorized
+ cu.execute("INSERT Societe X: X nom 'chouette'")
+ cu.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
+ self.commit()
+ cu.execute("DELETE A concerne S WHERE S nom 'chouette'")
+ self.commit()
def test_user_can_change_its_upassword(self):
req = self.request()
ueid = self.create_user(req, 'user').eid
- cnx = self.login('user')
- cu = cnx.cursor()
- cu.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
- {'x': ueid, 'passwd': 'newpwd'})
- cnx.commit()
- cnx.close()
+ with self.login('user') as cu:
+ cu.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
+ {'x': ueid, 'passwd': 'newpwd'})
+ self.commit()
cnx = self.login('user', password='newpwd')
cnx.close()
def test_user_cant_change_other_upassword(self):
req = self.request()
ueid = self.create_user(req, 'otheruser').eid
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- cu.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
- {'x': ueid, 'passwd': 'newpwd'})
- self.assertRaises(Unauthorized, cnx.commit)
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ cu.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
+ {'x': ueid, 'passwd': 'newpwd'})
+ self.assertRaises(Unauthorized, self.commit)
# read security test
def test_read_base(self):
- self.schema['Personne'].set_action_permissions('read', ('users', 'managers'))
- cnx = self.login('anon')
- cu = cnx.cursor()
- self.assertRaises(Unauthorized,
- cu.execute, 'Personne U where U nom "managers"')
- cnx.close()
+ with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
+ with self.login('anon') as cu:
+ self.assertRaises(Unauthorized,
+ cu.execute, 'Personne U where U nom "managers"')
+ self.rollback()
def test_read_erqlexpr_base(self):
eid = self.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
self.commit()
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- rset = cu.execute('Affaire X')
- self.assertEqual(rset.rows, [])
- self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
- # cache test
- self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
- aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
- soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
- cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
- cnx.commit()
- rset = cu.execute('Any X WHERE X eid %(x)s', {'x': aff2})
- self.assertEqual(rset.rows, [[aff2]])
- # more cache test w/ NOT eid
- rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': eid})
- self.assertEqual(rset.rows, [[aff2]])
- rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': aff2})
- self.assertEqual(rset.rows, [])
- # test can't update an attribute of an entity that can't be readen
- self.assertRaises(Unauthorized, cu.execute, 'SET X sujet "hacked" WHERE X eid %(x)s', {'x': eid})
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ rset = cu.execute('Affaire X')
+ self.assertEqual(rset.rows, [])
+ self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
+ # cache test
+ self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
+ aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+ soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
+ cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+ self.commit()
+ rset = cu.execute('Any X WHERE X eid %(x)s', {'x': aff2})
+ self.assertEqual(rset.rows, [[aff2]])
+ # more cache test w/ NOT eid
+ rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': eid})
+ self.assertEqual(rset.rows, [[aff2]])
+ rset = cu.execute('Affaire X WHERE NOT X eid %(x)s', {'x': aff2})
+ self.assertEqual(rset.rows, [])
+ # test can't update an attribute of an entity that can't be readen
+ self.assertRaises(Unauthorized, cu.execute, 'SET X sujet "hacked" WHERE X eid %(x)s', {'x': eid})
+ self.rollback()
def test_entity_created_in_transaction(self):
affschema = self.schema['Affaire']
- origperms = affschema.permissions['read']
- affschema.set_action_permissions('read', affschema.permissions['add'])
- try:
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
- # entity created in transaction are readable *by eid*
- self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
- # XXX would be nice if it worked
- rset = cu.execute("Affaire X WHERE X sujet 'cool'")
- self.assertEqual(len(rset), 0)
- finally:
- affschema.set_action_permissions('read', origperms)
- cnx.close()
+ with self.temporary_permissions(Affaire={'read': affschema.permissions['add']}):
+ with self.login('iaminusersgrouponly') as cu:
+ aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+ # entity created in transaction are readable *by eid*
+ self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
+ # XXX would be nice if it worked
+ rset = cu.execute("Affaire X WHERE X sujet 'cool'")
+ self.assertEqual(len(rset), 0)
+ self.assertRaises(Unauthorized, self.commit)
def test_read_erqlexpr_has_text1(self):
aff1 = self.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
card1 = self.execute("INSERT Card X: X title 'cool'")[0][0]
self.execute('SET X owned_by U WHERE X eid %(x)s, U login "iaminusersgrouponly"', {'x': card1})
self.commit()
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
- soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
- cu.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1})
- cnx.commit()
- self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x':aff1})
- self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
- self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':card1}))
- rset = cu.execute("Any X WHERE X has_text 'cool'")
- self.assertEqual(sorted(eid for eid, in rset.rows),
- [card1, aff2])
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+ soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
+ cu.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1})
+ self.commit()
+ self.assertRaises(Unauthorized, cu.execute, 'Any X WHERE X eid %(x)s', {'x':aff1})
+ self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
+ self.assertTrue(cu.execute('Any X WHERE X eid %(x)s', {'x':card1}))
+ rset = cu.execute("Any X WHERE X has_text 'cool'")
+ self.assertEqual(sorted(eid for eid, in rset.rows),
+ [card1, aff2])
+ self.rollback()
def test_read_erqlexpr_has_text2(self):
self.execute("INSERT Personne X: X nom 'bidule'")
self.execute("INSERT Societe X: X nom 'bidule'")
self.commit()
- self.schema['Personne'].set_action_permissions('read', ('managers',))
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- rset = cu.execute('Any N WHERE N has_text "bidule"')
- self.assertEqual(len(rset.rows), 1, rset.rows)
- rset = cu.execute('Any N WITH N BEING (Any N WHERE N has_text "bidule")')
- self.assertEqual(len(rset.rows), 1, rset.rows)
- cnx.close()
+ with self.temporary_permissions(Personne={'read': ('managers',)}):
+ with self.login('iaminusersgrouponly') as cu:
+ rset = cu.execute('Any N WHERE N has_text "bidule"')
+ self.assertEqual(len(rset.rows), 1, rset.rows)
+ rset = cu.execute('Any N WITH N BEING (Any N WHERE N has_text "bidule")')
+ self.assertEqual(len(rset.rows), 1, rset.rows)
def test_read_erqlexpr_optional_rel(self):
self.execute("INSERT Personne X: X nom 'bidule'")
self.execute("INSERT Societe X: X nom 'bidule'")
self.commit()
- self.schema['Personne'].set_action_permissions('read', ('managers',))
- cnx = self.login('anon')
- cu = cnx.cursor()
- rset = cu.execute('Any N,U WHERE N has_text "bidule", N owned_by U?')
- self.assertEqual(len(rset.rows), 1, rset.rows)
- cnx.close()
+ with self.temporary_permissions(Personne={'read': ('managers',)}):
+ with self.login('anon') as cu:
+ rset = cu.execute('Any N,U WHERE N has_text "bidule", N owned_by U?')
+ self.assertEqual(len(rset.rows), 1, rset.rows)
def test_read_erqlexpr_aggregat(self):
self.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
self.commit()
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- rset = cu.execute('Any COUNT(X) WHERE X is Affaire')
- self.assertEqual(rset.rows, [[0]])
- aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
- soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
- cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
- cnx.commit()
- rset = cu.execute('Any COUNT(X) WHERE X is Affaire')
- self.assertEqual(rset.rows, [[1]])
- rset = cu.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN')
- values = dict(rset)
- self.assertEqual(values['Affaire'], 1)
- self.assertEqual(values['Societe'], 2)
- rset = cu.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN WITH X BEING ((Affaire X) UNION (Societe X))')
- self.assertEqual(len(rset), 2)
- values = dict(rset)
- self.assertEqual(values['Affaire'], 1)
- self.assertEqual(values['Societe'], 2)
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ rset = cu.execute('Any COUNT(X) WHERE X is Affaire')
+ self.assertEqual(rset.rows, [[0]])
+ aff2 = cu.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+ soc1 = cu.execute("INSERT Societe X: X nom 'chouette'")[0][0]
+ cu.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+ self.commit()
+ rset = cu.execute('Any COUNT(X) WHERE X is Affaire')
+ self.assertEqual(rset.rows, [[1]])
+ rset = cu.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN')
+ values = dict(rset)
+ self.assertEqual(values['Affaire'], 1)
+ self.assertEqual(values['Societe'], 2)
+ rset = cu.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN WITH X BEING ((Affaire X) UNION (Societe X))')
+ self.assertEqual(len(rset), 2)
+ values = dict(rset)
+ self.assertEqual(values['Affaire'], 1)
+ self.assertEqual(values['Societe'], 2)
def test_attribute_security(self):
# only managers should be able to edit the 'test' attribute of Personne entities
eid = self.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org', X test TRUE")[0][0]
- self.commit()
self.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
self.commit()
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org', X test TRUE")
- self.assertRaises(Unauthorized, cnx.commit)
- cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org', X test FALSE")
- self.assertRaises(Unauthorized, cnx.commit)
- eid = cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org'")[0][0]
- cnx.commit()
- cu.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
- self.assertRaises(Unauthorized, cnx.commit)
- cu.execute('SET X test TRUE WHERE X eid %(x)s', {'x': eid})
- self.assertRaises(Unauthorized, cnx.commit)
- cu.execute('SET X web "http://www.logilab.org" WHERE X eid %(x)s', {'x': eid})
- cnx.commit()
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org', X test TRUE")
+ self.assertRaises(Unauthorized, self.commit)
+ cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org', X test FALSE")
+ self.assertRaises(Unauthorized, self.commit)
+ eid = cu.execute("INSERT Personne X: X nom 'bidule', X web 'http://www.debian.org'")[0][0]
+ self.commit()
+ cu.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
+ self.assertRaises(Unauthorized, self.commit)
+ cu.execute('SET X test TRUE WHERE X eid %(x)s', {'x': eid})
+ self.assertRaises(Unauthorized, self.commit)
+ cu.execute('SET X web "http://www.logilab.org" WHERE X eid %(x)s', {'x': eid})
+ self.commit()
def test_attribute_security_rqlexpr(self):
# Note.para attribute editable by managers or if the note is in "todo" state
@@ -432,54 +399,62 @@
note.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid})
self.commit()
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid})
- self.assertRaises(Unauthorized, cnx.commit)
- note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
- cnx.commit()
- note2.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
- cnx.commit()
- self.assertEqual(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid})),
- 0)
- cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
- self.assertRaises(Unauthorized, cnx.commit)
- note2.cw_adapt_to('IWorkflowable').fire_transition('redoit')
- cnx.commit()
- cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
- cnx.commit()
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid})
+ self.assertRaises(Unauthorized, self.commit)
+ note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
+ self.commit()
+ note2.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
+ self.commit()
+ self.assertEqual(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid})),
+ 0)
+ cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
+ self.assertRaises(Unauthorized, self.commit)
+ note2.cw_adapt_to('IWorkflowable').fire_transition('redoit')
+ self.commit()
+ cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
+ self.commit()
def test_attribute_read_security(self):
# anon not allowed to see users'login, but they can see users
- self.repo.schema['CWUser'].set_action_permissions('read', ('guests', 'users', 'managers'))
- self.repo.schema['CWUser'].rdef('login').set_action_permissions('read', ('users', 'managers'))
- cnx = self.login('anon')
- cu = cnx.cursor()
- rset = cu.execute('CWUser X')
- self.assertTrue(rset)
- x = rset.get_entity(0, 0)
- self.assertEqual(x.login, None)
- self.assertTrue(x.creation_date)
- x = rset.get_entity(1, 0)
- x.complete()
- self.assertEqual(x.login, None)
- self.assertTrue(x.creation_date)
- cnx.rollback()
- cnx.close()
+ login_rdef = self.repo.schema['CWUser'].rdef('login')
+ with self.temporary_permissions((login_rdef, {'read': ('users', 'managers')}),
+ CWUser={'read': ('guests', 'users', 'managers')}):
+ with self.login('anon') as cu:
+ rset = cu.execute('CWUser X')
+ self.assertTrue(rset)
+ x = rset.get_entity(0, 0)
+ self.assertEqual(x.login, None)
+ self.assertTrue(x.creation_date)
+ x = rset.get_entity(1, 0)
+ x.complete()
+ self.assertEqual(x.login, None)
+ self.assertTrue(x.creation_date)
+
+ def test_yams_inheritance_and_security_bug(self):
+ with self.temporary_permissions(Division={'read': ('managers', ERQLExpression('X owned_by U'))}):
+ with self.login('iaminusersgrouponly'):
+ querier = self.repo.querier
+ rqlst = querier.parse('Any X WHERE X is_instance_of Societe')
+ querier.solutions(self.session, rqlst, {})
+ querier._annotate(rqlst)
+ plan = querier.plan_factory(rqlst, {}, self.session)
+ plan.preprocess(rqlst)
+ self.assertEqual(
+ rqlst.as_string(),
+ '(Any X WHERE X is IN(SubDivision, Societe)) UNION (Any X WHERE X is Division, EXISTS(X owned_by %(B)s))')
+
class BaseSchemaSecurityTC(BaseSecurityTC):
"""tests related to the base schema permission configuration"""
def test_user_can_delete_object_he_created(self):
# even if some other user have changed object'state
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- # due to security test, affaire has to concerne a societe the user owns
- cu.execute('INSERT Societe X: X nom "ARCTIA"')
- cu.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"')
- cnx.commit()
- self.restore_connection()
+ with self.login('iaminusersgrouponly') as cu:
+ # due to security test, affaire has to concerne a societe the user owns
+ cu.execute('INSERT Societe X: X nom "ARCTIA"')
+ cu.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"')
+ self.commit()
affaire = self.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
affaire.cw_adapt_to('IWorkflowable').fire_transition('abort')
self.commit()
@@ -488,90 +463,79 @@
self.assertEqual(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",'
'X owned_by U, U login "admin"')),
1) # TrInfo at the above state change
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- cu.execute('DELETE Affaire X WHERE X ref "ARCT01"')
- cnx.commit()
- self.assertFalse(cu.execute('Affaire X'))
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ cu.execute('DELETE Affaire X WHERE X ref "ARCT01"')
+ self.commit()
+ self.assertFalse(cu.execute('Affaire X'))
def test_users_and_groups_non_readable_by_guests(self):
- cnx = self.login('anon')
- anon = cnx.user(self.session)
- cu = cnx.cursor()
- # anonymous user can only read itself
- rset = cu.execute('Any L WHERE X owned_by U, U login L')
- self.assertEqual(rset.rows, [['anon']])
- rset = cu.execute('CWUser X')
- self.assertEqual(rset.rows, [[anon.eid]])
- # anonymous user can read groups (necessary to check allowed transitions for instance)
- self.assert_(cu.execute('CWGroup X'))
- # should only be able to read the anonymous user, not another one
- origuser = self.adminsession.user
- self.assertRaises(Unauthorized,
- cu.execute, 'CWUser X WHERE X eid %(x)s', {'x': origuser.eid})
- # nothing selected, nothing updated, no exception raised
- #self.assertRaises(Unauthorized,
- # cu.execute, 'SET X login "toto" WHERE X eid %(x)s',
- # {'x': self.user.eid})
+ with self.login('anon') as cu:
+ anon = cu.connection.user(self.session)
+ # anonymous user can only read itself
+ rset = cu.execute('Any L WHERE X owned_by U, U login L')
+ self.assertEqual(rset.rows, [['anon']])
+ rset = cu.execute('CWUser X')
+ self.assertEqual(rset.rows, [[anon.eid]])
+ # anonymous user can read groups (necessary to check allowed transitions for instance)
+ self.assert_(cu.execute('CWGroup X'))
+ # should only be able to read the anonymous user, not another one
+ origuser = self.adminsession.user
+ self.assertRaises(Unauthorized,
+ cu.execute, 'CWUser X WHERE X eid %(x)s', {'x': origuser.eid})
+ # nothing selected, nothing updated, no exception raised
+ #self.assertRaises(Unauthorized,
+ # cu.execute, 'SET X login "toto" WHERE X eid %(x)s',
+ # {'x': self.user.eid})
- rset = cu.execute('CWUser X WHERE X eid %(x)s', {'x': anon.eid})
- self.assertEqual(rset.rows, [[anon.eid]])
- # but can't modify it
- cu.execute('SET X login "toto" WHERE X eid %(x)s', {'x': anon.eid})
- self.assertRaises(Unauthorized, cnx.commit)
- cnx.close()
+ rset = cu.execute('CWUser X WHERE X eid %(x)s', {'x': anon.eid})
+ self.assertEqual(rset.rows, [[anon.eid]])
+ # but can't modify it
+ cu.execute('SET X login "toto" WHERE X eid %(x)s', {'x': anon.eid})
+ self.assertRaises(Unauthorized, self.commit)
def test_in_group_relation(self):
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- rql = u"DELETE U in_group G WHERE U login 'admin'"
- self.assertRaises(Unauthorized, cu.execute, rql)
- rql = u"SET U in_group G WHERE U login 'admin', G name 'users'"
- self.assertRaises(Unauthorized, cu.execute, rql)
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ rql = u"DELETE U in_group G WHERE U login 'admin'"
+ self.assertRaises(Unauthorized, cu.execute, rql)
+ rql = u"SET U in_group G WHERE U login 'admin', G name 'users'"
+ self.assertRaises(Unauthorized, cu.execute, rql)
+ self.rollback()
def test_owned_by(self):
self.execute("INSERT Personne X: X nom 'bidule'")
self.commit()
- cnx = self.login('iaminusersgrouponly')
- cu = cnx.cursor()
- rql = u"SET X owned_by U WHERE U login 'iaminusersgrouponly', X is Personne"
- self.assertRaises(Unauthorized, cu.execute, rql)
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ rql = u"SET X owned_by U WHERE U login 'iaminusersgrouponly', X is Personne"
+ self.assertRaises(Unauthorized, cu.execute, rql)
+ self.rollback()
def test_bookmarked_by_guests_security(self):
beid1 = self.execute('INSERT Bookmark B: B path "?vid=manage", B title "manage"')[0][0]
beid2 = self.execute('INSERT Bookmark B: B path "?vid=index", B title "index", B bookmarked_by U WHERE U login "anon"')[0][0]
self.commit()
- cnx = self.login('anon')
- cu = cnx.cursor()
- anoneid = self.session.user.eid
- self.assertEqual(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
- 'B bookmarked_by U, U eid %s' % anoneid).rows,
- [['index', '?vid=index']])
- self.assertEqual(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
- 'B bookmarked_by U, U eid %(x)s', {'x': anoneid}).rows,
- [['index', '?vid=index']])
- # can read others bookmarks as well
- self.assertEqual(cu.execute('Any B where B is Bookmark, NOT B bookmarked_by U').rows,
- [[beid1]])
- self.assertRaises(Unauthorized, cu.execute,'DELETE B bookmarked_by U')
- self.assertRaises(Unauthorized,
- cu.execute, 'SET B bookmarked_by U WHERE U eid %(x)s, B eid %(b)s',
- {'x': anoneid, 'b': beid1})
- cnx.close()
-
+ with self.login('anon') as cu:
+ anoneid = self.session.user.eid
+ self.assertEqual(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
+ 'B bookmarked_by U, U eid %s' % anoneid).rows,
+ [['index', '?vid=index']])
+ self.assertEqual(cu.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
+ 'B bookmarked_by U, U eid %(x)s', {'x': anoneid}).rows,
+ [['index', '?vid=index']])
+ # can read others bookmarks as well
+ self.assertEqual(cu.execute('Any B where B is Bookmark, NOT B bookmarked_by U').rows,
+ [[beid1]])
+ self.assertRaises(Unauthorized, cu.execute,'DELETE B bookmarked_by U')
+ self.assertRaises(Unauthorized,
+ cu.execute, 'SET B bookmarked_by U WHERE U eid %(x)s, B eid %(b)s',
+ {'x': anoneid, 'b': beid1})
+ self.rollback()
def test_ambigous_ordered(self):
- cnx = self.login('anon')
- cu = cnx.cursor()
- names = [t for t, in cu.execute('Any N ORDERBY lower(N) WHERE X name N')]
- self.assertEqual(names, sorted(names, key=lambda x: x.lower()))
- cnx.close()
+ with self.login('anon') as cu:
+ names = [t for t, in cu.execute('Any N ORDERBY lower(N) WHERE X name N')]
+ self.assertEqual(names, sorted(names, key=lambda x: x.lower()))
def test_restrict_is_instance_ok(self):
- from rql import RQLException
rset = self.execute('Any X WHERE X is_instance_of BaseTransition')
rqlst = rset.syntax_tree()
select = rqlst.children[0]
@@ -596,33 +560,28 @@
"""
eid = self.execute('INSERT Affaire X: X ref "ARCT01"')[0][0]
self.commit()
- cnx = self.login('iaminusersgrouponly')
- session = self.session
- # needed to avoid check_perm error
- session.set_cnxset()
- # needed to remove rql expr granting update perm to the user
- affaire_perms = self.schema['Affaire'].permissions.copy()
- self.schema['Affaire'].set_action_permissions('update', self.schema['Affaire'].get_groups('update'))
- try:
- self.assertRaises(Unauthorized,
- self.schema['Affaire'].check_perm, session, 'update', eid=eid)
- cu = cnx.cursor()
- self.schema['Affaire'].set_action_permissions('read', ('users',))
- aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
- aff.cw_adapt_to('IWorkflowable').fire_transition('abort')
- cnx.commit()
- # though changing a user state (even logged user) is reserved to managers
- user = cnx.user(self.session)
- # XXX wether it should raise Unauthorized or ValidationError is not clear
- # the best would probably ValidationError if the transition doesn't exist
- # from the current state but Unauthorized if it exists but user can't pass it
- self.assertRaises(ValidationError,
- user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate')
- finally:
- # restore orig perms
- for action, perms in affaire_perms.iteritems():
- self.schema['Affaire'].set_action_permissions(action, perms)
- cnx.close()
+ with self.login('iaminusersgrouponly') as cu:
+ session = self.session
+ # needed to avoid check_perm error
+ session.set_cnxset()
+ # needed to remove rql expr granting update perm to the user
+ affschema = self.schema['Affaire']
+ with self.temporary_permissions(Affaire={'update': affschema.get_groups('update'),
+ 'read': ('users',)}):
+ self.assertRaises(Unauthorized,
+ affschema.check_perm, session, 'update', eid=eid)
+ aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
+ aff.cw_adapt_to('IWorkflowable').fire_transition('abort')
+ self.commit()
+ # though changing a user state (even logged user) is reserved to managers
+ user = self.user(session)
+ session.set_cnxset()
+ # XXX wether it should raise Unauthorized or ValidationError is not clear
+ # the best would probably ValidationError if the transition doesn't exist
+ # from the current state but Unauthorized if it exists but user can't pass it
+ self.assertRaises(ValidationError,
+ user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate')
+ self.rollback() # else will fail on login cm exit
def test_trinfo_security(self):
aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0)
--- a/server/test/unittest_session.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_session.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -17,33 +17,7 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
from __future__ import with_statement
-from logilab.common.testlib import TestCase, unittest_main, mock_object
-
from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.server.session import _make_description, hooks_control
-
-class Variable:
- def __init__(self, name):
- self.name = name
- self.children = []
-
- def get_type(self, solution, args=None):
- return solution[self.name]
- def as_string(self):
- return self.name
-
-class Function:
- def __init__(self, name, varname):
- self.name = name
- self.children = [Variable(varname)]
- def get_type(self, solution, args=None):
- return 'Int'
-
-class MakeDescriptionTC(TestCase):
- def test_known_values(self):
- solution = {'A': 'Int', 'B': 'CWUser'}
- self.assertEqual(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
- ['Int','CWUser'])
class InternalSessionTC(CubicWebTC):
@@ -61,7 +35,7 @@
self.assertEqual(session.disabled_hook_categories, set())
self.assertEqual(session.enabled_hook_categories, set())
self.assertEqual(len(session._tx_data), 1)
- with hooks_control(session, session.HOOKS_DENY_ALL, 'metadata'):
+ with session.deny_all_hooks_but('metadata'):
self.assertEqual(session.hooks_mode, session.HOOKS_DENY_ALL)
self.assertEqual(session.disabled_hook_categories, set())
self.assertEqual(session.enabled_hook_categories, set(('metadata',)))
@@ -73,7 +47,7 @@
self.assertEqual(session.hooks_mode, session.HOOKS_DENY_ALL)
self.assertEqual(session.disabled_hook_categories, set())
self.assertEqual(session.enabled_hook_categories, set(('metadata',)))
- with hooks_control(session, session.HOOKS_ALLOW_ALL, 'integrity'):
+ with session.allow_all_hooks_but('integrity'):
self.assertEqual(session.hooks_mode, session.HOOKS_ALLOW_ALL)
self.assertEqual(session.disabled_hook_categories, set(('integrity',)))
self.assertEqual(session.enabled_hook_categories, set(('metadata',))) # not changed in such case
@@ -88,27 +62,7 @@
self.assertEqual(session.disabled_hook_categories, set())
self.assertEqual(session.enabled_hook_categories, set())
- def test_build_descr1(self):
- rset = self.execute('(Any U,L WHERE U login L) UNION (Any G,N WHERE G name N, G is CWGroup)')
- orig_length = len(rset)
- rset.rows[0][0] = 9999999
- description = self.session.build_description(rset.syntax_tree(), None, rset.rows)
- self.assertEqual(len(description), orig_length - 1)
- self.assertEqual(len(rset.rows), orig_length - 1)
- self.assertFalse(rset.rows[0][0] == 9999999)
-
- def test_build_descr2(self):
- rset = self.execute('Any X,Y WITH X,Y BEING ((Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G))')
- for x, y in rset.description:
- if y is not None:
- self.assertEqual(y, 'CWGroup')
-
- def test_build_descr3(self):
- rset = self.execute('(Any G,NULL WHERE G is CWGroup) UNION (Any U,G WHERE U in_group G)')
- for x, y in rset.description:
- if y is not None:
- self.assertEqual(y, 'CWGroup')
-
if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
unittest_main()
--- a/server/test/unittest_storage.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_storage.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -99,7 +99,7 @@
f1 = self.create_file()
self.commit()
self.assertEqual(file(expected_filepath).read(), 'the-data')
- f1.set_attributes(data=Binary('the new data'))
+ f1.cw_set(data=Binary('the new data'))
self.rollback()
self.assertEqual(file(expected_filepath).read(), 'the-data')
f1.cw_delete()
@@ -204,7 +204,7 @@
# use self.session to use server-side cache
f1 = self.session.create_entity('File', data=Binary('some data'),
data_format=u'text/plain', data_name=u'foo')
- # NOTE: do not use set_attributes() which would automatically
+ # NOTE: do not use cw_set() which would automatically
# update f1's local dict. We want the pure rql version to work
self.execute('SET F data %(d)s WHERE F eid %(f)s',
{'d': Binary('some other data'), 'f': f1.eid})
@@ -218,7 +218,7 @@
# use self.session to use server-side cache
f1 = self.session.create_entity('File', data=Binary('some data'),
data_format=u'text/plain', data_name=u'foo.txt')
- # NOTE: do not use set_attributes() which would automatically
+ # NOTE: do not use cw_set() which would automatically
# update f1's local dict. We want the pure rql version to work
self.commit()
old_path = self.fspath(f1)
@@ -240,7 +240,7 @@
# use self.session to use server-side cache
f1 = self.session.create_entity('File', data=Binary('some data'),
data_format=u'text/plain', data_name=u'foo.txt')
- # NOTE: do not use set_attributes() which would automatically
+ # NOTE: do not use cw_set() which would automatically
# update f1's local dict. We want the pure rql version to work
self.commit()
old_path = self.fspath(f1)
@@ -265,7 +265,7 @@
f = self.session.create_entity('Affaire', opt_attr=Binary('toto'))
self.session.commit()
self.session.set_cnxset()
- f.set_attributes(opt_attr=None)
+ f.cw_set(opt_attr=None)
self.session.commit()
@tag('fs_importing', 'update')
--- a/server/test/unittest_undo.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/test/unittest_undo.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -22,14 +22,14 @@
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.transaction import *
-from cubicweb.server.sources.native import UndoException
+from cubicweb.server.sources.native import UndoTransactionException, _UndoException
class UndoableTransactionTC(CubicWebTC):
def setup_database(self):
req = self.request()
- self.session.undo_actions = set('CUDAR')
+ self.session.undo_actions = True
self.toto = self.create_user(req, 'toto', password='toto', groups=('users',),
commit=False)
self.txuuid = self.commit()
@@ -48,6 +48,17 @@
"SELECT * from tx_relation_actions WHERE tx_uuid='%s'" % txuuid)
self.assertFalse(cu.fetchall())
+ def assertUndoTransaction(self, txuuid, expected_errors=None):
+ if expected_errors is None :
+ expected_errors = []
+ try:
+ self.cnx.undo_transaction(txuuid)
+ except UndoTransactionException, exn:
+ errors = exn.errors
+ else:
+ errors = []
+ self.assertEqual(errors, expected_errors)
+
def test_undo_api(self):
self.assertTrue(self.txuuid)
# test transaction api
@@ -69,12 +80,14 @@
self.assertEqual(a1.action, 'C')
self.assertEqual(a1.eid, self.toto.eid)
self.assertEqual(a1.etype,'CWUser')
+ self.assertEqual(a1.ertype, 'CWUser')
self.assertEqual(a1.changes, None)
self.assertEqual(a1.public, True)
self.assertEqual(a1.order, 1)
a4 = actions[3]
self.assertEqual(a4.action, 'A')
self.assertEqual(a4.rtype, 'in_group')
+ self.assertEqual(a4.ertype, 'in_group')
self.assertEqual(a4.eid_from, self.toto.eid)
self.assertEqual(a4.eid_to, self.toto.in_group[0].eid)
self.assertEqual(a4.order, 4)
@@ -155,10 +168,9 @@
self.assertEqual(len(actions), 1)
toto.cw_clear_all_caches()
e.cw_clear_all_caches()
- errors = self.cnx.undo_transaction(txuuid)
+ self.assertUndoTransaction(txuuid)
undotxuuid = self.commit()
self.assertEqual(undotxuuid, None) # undo not undoable
- self.assertEqual(errors, [])
self.assertTrue(self.execute('Any X WHERE X eid %(x)s', {'x': toto.eid}))
self.assertTrue(self.execute('Any X WHERE X eid %(x)s', {'x': e.eid}))
self.assertTrue(self.execute('Any X WHERE X has_text "toto@logilab"'))
@@ -191,34 +203,32 @@
c.cw_delete()
txuuid = self.commit()
c2 = session.create_entity('Card', title=u'hip', content=u'hip')
- p.set_relations(fiche=c2)
+ p.cw_set(fiche=c2)
self.commit()
- errors = self.cnx.undo_transaction(txuuid)
+ self.assertUndoTransaction(txuuid, [
+ "Can't restore object relation fiche to entity "
+ "%s which is already linked using this relation." % p.eid])
self.commit()
p.cw_clear_all_caches()
self.assertEqual(p.fiche[0].eid, c2.eid)
- self.assertEqual(len(errors), 1)
- self.assertEqual(errors[0],
- "Can't restore object relation fiche to entity "
- "%s which is already linked using this relation." % p.eid)
def test_undo_deletion_integrity_2(self):
# test validation error raised if we can't restore a required relation
session = self.session
g = session.create_entity('CWGroup', name=u'staff')
session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid})
- self.toto.set_relations(in_group=g)
+ self.toto.cw_set(in_group=g)
self.commit()
self.toto.cw_delete()
txuuid = self.commit()
g.cw_delete()
self.commit()
- errors = self.cnx.undo_transaction(txuuid)
- self.assertEqual(errors,
- [u"Can't restore relation in_group, object entity "
- "%s doesn't exist anymore." % g.eid])
+ self.assertUndoTransaction(txuuid, [
+ u"Can't restore relation in_group, object entity "
+ "%s doesn't exist anymore." % g.eid])
with self.assertRaises(ValidationError) as cm:
self.commit()
+ cm.exception.tr(unicode)
self.assertEqual(cm.exception.entity, self.toto.eid)
self.assertEqual(cm.exception.errors,
{'in_group-subject': u'at least one relation in_group is '
@@ -229,9 +239,8 @@
c = session.create_entity('Card', title=u'hop', content=u'hop')
p = session.create_entity('Personne', nom=u'louis', fiche=c)
txuuid = self.commit()
- errors = self.cnx.undo_transaction(txuuid)
+ self.assertUndoTransaction(txuuid)
self.commit()
- self.assertFalse(errors)
self.assertFalse(self.execute('Any X WHERE X eid %(x)s', {'x': c.eid}))
self.assertFalse(self.execute('Any X WHERE X eid %(x)s', {'x': p.eid}))
self.assertFalse(self.execute('Any X,Y WHERE X fiche Y'))
@@ -257,7 +266,7 @@
email = self.request().create_entity('EmailAddress', address=u'tutu@cubicweb.org')
prop = self.request().create_entity('CWProperty', pkey=u'ui.default-text-format',
value=u'text/html')
- tutu.set_relations(use_email=email, reverse_for_user=prop)
+ tutu.cw_set(use_email=email, reverse_for_user=prop)
self.commit()
with self.assertRaises(ValidationError) as cm:
self.cnx.undo_transaction(txuuid)
@@ -270,7 +279,7 @@
g = session.create_entity('CWGroup', name=u'staff')
txuuid = self.commit()
session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid})
- self.toto.set_relations(in_group=g)
+ self.toto.cw_set(in_group=g)
self.commit()
with self.assertRaises(ValidationError) as cm:
self.cnx.undo_transaction(txuuid)
@@ -288,12 +297,135 @@
# test implicit 'replacement' of an inlined relation
+ def test_undo_inline_rel_remove_ok(self):
+ """Undo remove relation Personne (?) fiche (?) Card
+
+ NB: processed by `_undo_r` as expected"""
+ session = self.session
+ c = session.create_entity('Card', title=u'hop', content=u'hop')
+ p = session.create_entity('Personne', nom=u'louis', fiche=c)
+ self.commit()
+ p.cw_set(fiche=None)
+ txuuid = self.commit()
+ self.assertUndoTransaction(txuuid)
+ self.commit()
+ p.cw_clear_all_caches()
+ self.assertEqual(p.fiche[0].eid, c.eid)
+
+ def test_undo_inline_rel_remove_ko(self):
+ """Restore an inlined relation to a deleted entity, with an error.
+
+ NB: processed by `_undo_r` as expected"""
+ session = self.session
+ c = session.create_entity('Card', title=u'hop', content=u'hop')
+ p = session.create_entity('Personne', nom=u'louis', fiche=c)
+ self.commit()
+ p.cw_set(fiche=None)
+ txuuid = self.commit()
+ c.cw_delete()
+ self.commit()
+ self.assertUndoTransaction(txuuid, [
+ "Can't restore relation fiche, object entity %d doesn't exist anymore." % c.eid])
+ self.commit()
+ p.cw_clear_all_caches()
+ self.assertFalse(p.fiche)
+ self.assertIsNone(session.system_sql(
+ 'SELECT cw_fiche FROM cw_Personne WHERE cw_eid=%s' % p.eid).fetchall()[0][0])
+
+ def test_undo_inline_rel_add_ok(self):
+ """Undo add relation Personne (?) fiche (?) Card
+
+ Caution processed by `_undo_u`, not `_undo_a` !"""
+ session = self.session
+ c = session.create_entity('Card', title=u'hop', content=u'hop')
+ p = session.create_entity('Personne', nom=u'louis')
+ self.commit()
+ p.cw_set(fiche=c)
+ txuuid = self.commit()
+ self.assertUndoTransaction(txuuid)
+ self.commit()
+ p.cw_clear_all_caches()
+ self.assertFalse(p.fiche)
+
+ def test_undo_inline_rel_add_ko(self):
+ """Undo add relation Personne (?) fiche (?) Card
+
+ Caution processed by `_undo_u`, not `_undo_a` !"""
+ session = self.session
+ c = session.create_entity('Card', title=u'hop', content=u'hop')
+ p = session.create_entity('Personne', nom=u'louis')
+ self.commit()
+ p.cw_set(fiche=c)
+ txuuid = self.commit()
+ c.cw_delete()
+ self.commit()
+ self.assertUndoTransaction(txuuid)
+
+ def test_undo_inline_rel_replace_ok(self):
+ """Undo changing relation Personne (?) fiche (?) Card
+
+ Caution processed by `_undo_u` """
+ session = self.session
+ c1 = session.create_entity('Card', title=u'hop', content=u'hop')
+ c2 = session.create_entity('Card', title=u'hip', content=u'hip')
+ p = session.create_entity('Personne', nom=u'louis', fiche=c1)
+ self.commit()
+ p.cw_set(fiche=c2)
+ txuuid = self.commit()
+ self.assertUndoTransaction(txuuid)
+ self.commit()
+ p.cw_clear_all_caches()
+ self.assertEqual(p.fiche[0].eid, c1.eid)
+
+ def test_undo_inline_rel_replace_ko(self):
+ """Undo changing relation Personne (?) fiche (?) Card, with an error
+
+ Caution processed by `_undo_u` """
+ session = self.session
+ c1 = session.create_entity('Card', title=u'hop', content=u'hop')
+ c2 = session.create_entity('Card', title=u'hip', content=u'hip')
+ p = session.create_entity('Personne', nom=u'louis', fiche=c1)
+ self.commit()
+ p.cw_set(fiche=c2)
+ txuuid = self.commit()
+ c1.cw_delete()
+ self.commit()
+ self.assertUndoTransaction(txuuid, [
+ "can't restore entity %s of type Personne, target of fiche (eid %s)"
+ " does not exist any longer" % (p.eid, c1.eid)])
+ self.commit()
+ p.cw_clear_all_caches()
+ self.assertFalse(p.fiche)
+
+ def test_undo_attr_update_ok(self):
+ session = self.session
+ p = session.create_entity('Personne', nom=u'toto')
+ session.commit()
+ self.session.set_cnxset()
+ p.cw_set(nom=u'titi')
+ txuuid = self.commit()
+ self.assertUndoTransaction(txuuid)
+ p.cw_clear_all_caches()
+ self.assertEqual(p.nom, u'toto')
+
+ def test_undo_attr_update_ko(self):
+ session = self.session
+ p = session.create_entity('Personne', nom=u'toto')
+ session.commit()
+ self.session.set_cnxset()
+ p.cw_set(nom=u'titi')
+ txuuid = self.commit()
+ p.cw_delete()
+ self.commit()
+ self.assertUndoTransaction(txuuid, [
+ u"can't restore state of entity %s, it has been deleted inbetween" % p.eid])
+
class UndoExceptionInUnicode(CubicWebTC):
# problem occurs in string manipulation for python < 2.6
def test___unicode__method(self):
- u = UndoException(u"voilà ")
+ u = _UndoException(u"voilà ")
self.assertIsInstance(unicode(u), unicode)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/test/unittest_utils.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,43 @@
+# copyright 2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""
+
+"""
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.server import utils
+
+class UtilsTC(TestCase):
+ def test_crypt(self):
+ for hash in (
+ utils.crypt_password('xxx'), # default sha512
+ 'ab$5UsKFxRKKN.d8iBIFBnQ80', # custom md5
+ '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', unicode(hash)), hash)
+ self.assertEqual(utils.crypt_password('yyy', hash), '')
+
+ # accept any password for empty hashes (is it a good idea?)
+ self.assertEqual(utils.crypt_password('xxx', ''), '')
+ self.assertEqual(utils.crypt_password('yyy', ''), '')
+
+
+if __name__ == '__main__':
+ unittest_main()
--- a/server/utils.py Wed Feb 22 11:57:42 2012 +0100
+++ b/server/utils.py Tue Oct 23 15:00:53 2012 +0200
@@ -20,35 +20,57 @@
__docformat__ = "restructuredtext en"
import sys
-import string
import logging
from threading import Timer, Thread
from getpass import getpass
-from random import choice
-
-from cubicweb.server import SOURCE_TYPES
-try:
- from crypt import crypt
-except ImportError:
- # crypt is not available (eg windows)
- from cubicweb.md5crypt import crypt
+from passlib.utils import handlers as uh, to_hash_str
+from passlib.context import CryptContext
+
+from cubicweb.md5crypt import crypt as md5crypt
-def getsalt(chars=string.letters + string.digits):
- """generate a random 2-character 'salt'"""
- return choice(chars) + choice(chars)
+class CustomMD5Crypt(uh.HasSalt, uh.GenericHandler):
+ name = 'cubicwebmd5crypt'
+ setting_kwds = ('salt',)
+ min_salt_size = 0
+ max_salt_size = 8
+ salt_chars = uh.H64_CHARS
+ @classmethod
+ def from_string(cls, hash):
+ salt, chk = uh.parse_mc2(hash, u'')
+ if chk is None:
+ raise ValueError('missing checksum')
+ return cls(salt=salt, checksum=chk)
+
+ def to_string(self):
+ return to_hash_str(u'%s$%s' % (self.salt, self.checksum or u''))
+
+ # passlib 1.5 wants calc_checksum, 1.6 wants _calc_checksum
+ def calc_checksum(self, secret):
+ 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 passwd is None:
- return None
if salt is None:
- salt = getsalt()
- return crypt(passwd, salt)
-
+ return _CRYPTO_CTX.encrypt(passwd)
+ # 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
+ pass
+ # wrong password
+ return ''
def cartesian_product(seqin):
"""returns a generator which returns the cartesian product of `seqin`
@@ -122,12 +144,12 @@
class LoopTask(object):
"""threaded task restarting itself once executed"""
- def __init__(self, repo, interval, func, args):
+ 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.repo = repo
+ self._tasks_manager = tasks_manager
self.interval = interval
def auto_restart_func(self=self, func=func, args=args):
restart = True
@@ -140,7 +162,7 @@
except BaseException:
restart = False
finally:
- if restart and not self.repo.shutting_down:
+ if restart and tasks_manager.running:
self.start()
self.func = auto_restart_func
self.name = func_name(func)
@@ -186,3 +208,54 @@
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.
+
+ looping tasks can only be registered during repository initialization,
+ once done this method will fail.
+ """
+ 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/skeleton/__pkginfo__.py.tmpl Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/__pkginfo__.py.tmpl Tue Oct 23 15:00:53 2012 +0200
@@ -16,6 +16,12 @@
__depends__ = %(dependencies)s
__recommends__ = {}
+classifiers = [
+ 'Environment :: Web Environment',
+ 'Framework :: CubicWeb',
+ 'Programming Language :: Python',
+ 'Programming Language :: JavaScript',
+ ]
from os import listdir as _listdir
from os.path import join, isdir
--- a/skeleton/debian/DISTNAME.prerm.tmpl Wed Feb 22 11:57:42 2012 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-#!/bin/sh -e
-
-delete_pyo_pyc () {
- find /usr/share/cubicweb/cubes/%(cubename)s -name "*.pyc" | xargs rm -f
- find /usr/share/cubicweb/cubes/%(cubename)s -name "*.pyo" | xargs rm -f
-}
-
-
-case "$1" in
- failed-upgrade|abort-install|abort-upgrade|disappear)
- ;;
- upgrade)
- delete_pyo_pyc
- ;;
- remove)
- delete_pyo_pyc
- ;;
- purge)
- ;;
-
- *)
- echo "postrm called with unknown argument \`$1'" >&2
- exit 1
-
-esac
-
-#DEBHELPER#
--- a/skeleton/debian/compat Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/debian/compat Tue Oct 23 15:00:53 2012 +0200
@@ -1,1 +1,1 @@
-5
+7
--- a/skeleton/debian/control.tmpl Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/debian/control.tmpl Tue Oct 23 15:00:53 2012 +0200
@@ -2,13 +2,13 @@
Section: web
Priority: optional
Maintainer: %(author)s <%(author-email)s>
-Build-Depends: debhelper (>= 5.0.37.1), python (>=2.4), python-support
-Standards-Version: 3.8.0
-
+Build-Depends: debhelper (>= 7), python (>=2.5), python-support
+Standards-Version: 3.9.3
+XS-Python-Version: >= 2.5
Package: %(distname)s
Architecture: all
-Depends: cubicweb-common (>= %(version)s)
+Depends: cubicweb-common (>= %(version)s), ${python:Depends}
Description: %(shortdesc)s
CubicWeb is a semantic web application framework.
.
--- a/skeleton/debian/copyright.tmpl Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/debian/copyright.tmpl Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-Upstream Author:
+Upstream Author:
%(author)s <%(author-email)s>
--- a/skeleton/debian/rules.tmpl Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/debian/rules.tmpl Tue Oct 23 15:00:53 2012 +0200
@@ -4,7 +4,10 @@
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
-build: build-stamp
+build: build-arch build-indep
+build-arch:
+ # Nothing to do
+build-indep: build-stamp
build-stamp:
dh_testdir
NO_SETUPTOOLS=1 python setup.py -q build
--- a/skeleton/setup.py Wed Feb 22 11:57:42 2012 +0100
+++ b/skeleton/setup.py Tue Oct 23 15:00:53 2012 +0200
@@ -41,7 +41,7 @@
# import required features
from __pkginfo__ import modname, version, license, description, web, \
- author, author_email
+ author, author_email, classifiers
if exists('README'):
long_description = file('README').read()
@@ -193,6 +193,7 @@
data_files = data_files,
ext_modules = ext_modules,
cmdclass = cmdclass,
+ classifiers = classifiers,
**kwargs
)
--- a/sobjects/cwxmlparser.py Wed Feb 22 11:57:42 2012 +0100
+++ b/sobjects/cwxmlparser.py Tue Oct 23 15:00:53 2012 +0200
@@ -183,11 +183,11 @@
# import handling ##########################################################
- def process(self, url, raise_on_error=False, partialcommit=True):
+ def process(self, url, raise_on_error=False):
"""IDataFeedParser main entry point"""
if url.startswith('http'): # XXX similar loose test as in parse of sources.datafeed
url = self.complete_url(url)
- super(CWEntityXMLParser, self).process(url, raise_on_error, partialcommit)
+ super(CWEntityXMLParser, self).process(url, raise_on_error)
def parse_etree(self, parent):
for node in list(parent):
@@ -242,7 +242,7 @@
def normalize_url(self, url):
"""overriden to add vid=xml"""
url = super(CWEntityXMLParser, self).normalize_url(url)
- if url.startswih('http'):
+ if url.startswith('http'):
try:
url, qs = url.split('?', 1)
except ValueError:
--- a/sobjects/ldapparser.py Wed Feb 22 11:57:42 2012 +0100
+++ b/sobjects/ldapparser.py Tue Oct 23 15:00:53 2012 +0200
@@ -20,41 +20,84 @@
unlike ldapuser source, this source is copy based and will import ldap content
(beside passwords for authentication) into the system source.
"""
-from base64 import b64decode
+from __future__ import with_statement
from logilab.common.decorators import cached
+from logilab.common.shellutils import generate_password
+from cubicweb import Binary, ConfigurationError
+from cubicweb.server.utils import crypt_password
from cubicweb.server.sources import datafeed
-class DataFeedlDAPParser(datafeed.DataFeedParser):
+
+class DataFeedLDAPAdapter(datafeed.DataFeedParser):
__regid__ = 'ldapfeed'
# attributes that may appears in source user_attrs dict which are not
# attributes of the cw user
non_attribute_keys = set(('email',))
- def process(self, url, raise_on_error=False, partialcommit=True):
+ def process(self, url, raise_on_error=False):
"""IDataFeedParser main entry point"""
source = self.source
searchstr = '(&%s)' % ''.join(source.base_filters)
- try:
- ldap_emailattr = source.user_rev_attrs['email']
- except KeyError:
- ldap_emailattr = None
+ self.warning('processing ldapfeed stuff %s %s', source, searchstr)
for userdict in source._search(self._cw, source.user_base_dn,
source.user_base_scope, searchstr):
+ self.warning('fetched user %s', userdict)
entity = self.extid2entity(userdict['dn'], 'CWUser', **userdict)
- if not self.created_during_pull(entity):
+ if entity is not None and not self.created_during_pull(entity):
self.notify_updated(entity)
attrs = self.ldap2cwattrs(userdict)
self.update_if_necessary(entity, attrs)
self._process_email(entity, userdict)
+
+ def handle_deletion(self, config, session, myuris):
+ if config['delete-entities']:
+ super(DataFeedLDAPAdapter, self).handle_deletion(config, session, myuris)
+ return
+ if myuris:
+ byetype = {}
+ for extid, (eid, etype) in myuris.iteritems():
+ if self.is_deleted(extid, etype, eid):
+ byetype.setdefault(etype, []).append(str(eid))
+
+ for etype, eids in byetype.iteritems():
+ if etype != 'CWUser':
+ continue
+ self.warning('deactivate %s %s entities', len(eids), etype)
+ for eid in eids:
+ wf = session.entity_from_eid(eid).cw_adapt_to('IWorkflowable')
+ wf.fire_transition_if_possible('deactivate')
+ session.commit(free_cnxset=False)
+
+ def update_if_necessary(self, entity, attrs):
+ # disable read security to allow password selection
+ with entity._cw.security_enabled(read=False):
+ entity.complete(tuple(attrs))
+ if entity.__regid__ == 'CWUser':
+ wf = entity.cw_adapt_to('IWorkflowable')
+ if wf.state == 'deactivated':
+ self.warning('update on deactivated user %s', entity.login)
+ mdate = attrs.get('modification_date')
+ if not mdate or mdate > entity.modification_date:
+ attrs = dict( (k, v) for k, v in attrs.iteritems()
+ if v != getattr(entity, k))
+ if attrs:
+ entity.cw_set(**attrs)
+ self.notify_updated(entity)
+
def ldap2cwattrs(self, sdict, tdict=None):
if tdict is None:
tdict = {}
for sattr, tattr in self.source.user_attrs.iteritems():
if tattr not in self.non_attribute_keys:
- tdict[tattr] = sdict[sattr]
+ try:
+ tdict[tattr] = sdict[sattr]
+ except KeyError:
+ raise ConfigurationError('source attribute %s is not present '
+ 'in the source, please check the '
+ 'user-attrs-map field' % sattr)
return tdict
def before_entity_copy(self, entity, sourceparams):
@@ -62,14 +105,20 @@
entity.cw_edited['address'] = sourceparams['address']
else:
self.ldap2cwattrs(sourceparams, entity.cw_edited)
+ pwd = entity.cw_edited.get('upassword')
+ if not pwd:
+ # generate a dumb password if not fetched from ldap (see
+ # userPassword)
+ pwd = crypt_password(generate_password())
+ entity.cw_edited['upassword'] = Binary(pwd)
return entity
def after_entity_copy(self, entity, sourceparams):
- super(DataFeedlDAPParser, self).after_entity_copy(entity, sourceparams)
+ super(DataFeedLDAPAdapter, self).after_entity_copy(entity, sourceparams)
if entity.__regid__ == 'EmailAddress':
return
groups = [self._get_group(n) for n in self.source.user_default_groups]
- entity.set_relations(in_group=groups)
+ entity.cw_set(in_group=groups)
self._process_email(entity, sourceparams)
def is_deleted(self, extid, etype, eid):
@@ -77,7 +126,7 @@
extid, _ = extid.rsplit('@@', 1)
except ValueError:
pass
- return self.source.object_exists_in_ldap(extid)
+ return not self.source.object_exists_in_ldap(extid)
def _process_email(self, entity, userdict):
try:
@@ -96,9 +145,13 @@
email = self.extid2entity(emailextid, 'EmailAddress',
address=emailaddr)
if entity.primary_email:
- entity.set_relations(use_email=email)
+ entity.cw_set(use_email=email)
else:
- entity.set_relations(primary_email=email)
+ entity.cw_set(primary_email=email)
+ elif self.sourceuris:
+ # pop from sourceuris anyway, else email may be removed by the
+ # source once import is finished
+ self.sourceuris.pop(str(userdict['dn'] + '@@' + emailaddr), None)
# XXX else check use_email relation?
@cached
--- a/sobjects/test/unittest_cwxmlparser.py Wed Feb 22 11:57:42 2012 +0100
+++ b/sobjects/test/unittest_cwxmlparser.py Tue Oct 23 15:00:53 2012 +0200
@@ -197,7 +197,7 @@
})
session = self.repo.internal_session(safe=True)
stats = dfsource.pull_data(session, force=True, raise_on_error=True)
- self.assertEqual(sorted(stats.keys()), ['created', 'updated'])
+ self.assertEqual(sorted(stats.keys()), ['checked', 'created', 'updated'])
self.assertEqual(len(stats['created']), 2)
self.assertEqual(stats['updated'], set())
@@ -233,14 +233,16 @@
with session.security_enabled(read=False): # avoid Unauthorized due to password selection
stats = dfsource.pull_data(session, force=True, raise_on_error=True)
self.assertEqual(stats['created'], set())
- self.assertEqual(len(stats['updated']), 2)
+ self.assertEqual(len(stats['updated']), 0)
+ self.assertEqual(len(stats['checked']), 2)
self.repo._type_source_cache.clear()
self.repo._extid_cache.clear()
session.set_cnxset()
with session.security_enabled(read=False): # avoid Unauthorized due to password selection
stats = dfsource.pull_data(session, force=True, raise_on_error=True)
self.assertEqual(stats['created'], set())
- self.assertEqual(len(stats['updated']), 2)
+ self.assertEqual(len(stats['updated']), 0)
+ self.assertEqual(len(stats['checked']), 2)
session.commit()
# test move to system source
--- a/test/data/views.py Wed Feb 22 11:57:42 2012 +0100
+++ b/test/data/views.py Tue Oct 23 15:00:53 2012 +0200
@@ -17,3 +17,17 @@
# 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
+
+
+class TestService(Service):
+ __regid__ = 'test_service'
+ __select__ = Service.__select__ & match_user_groups('managers')
+ passed_here = []
+
+ def call(self, msg):
+ self.passed_here.append(msg)
+ return 'babar'
--- a/test/unittest_cwconfig.py Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_cwconfig.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -101,10 +101,10 @@
self.assertEqual(self.config.expand_cubes(('email', 'comment')),
['email', 'comment', 'file'])
- def test_vregistry_path(self):
+ def test_appobjects_path(self):
self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
self.config.adjust_sys_path()
- self.assertEqual([unabsolutize(p) for p in self.config.vregistry_path()],
+ self.assertEqual([unabsolutize(p) for p in self.config.appobjects_path()],
['entities', 'web/views', 'sobjects', 'hooks',
'file/entities', 'file/views.py', 'file/hooks',
'email/entities.py', 'email/views', 'email/hooks.py',
--- a/test/unittest_dbapi.py Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_dbapi.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -23,10 +23,11 @@
from logilab.common import tempattr
-from cubicweb import ConnectionError, cwconfig
+from cubicweb import ConnectionError, cwconfig, NoSelectableObject
from cubicweb.dbapi import ProgrammingError
from cubicweb.devtools.testlib import CubicWebTC
+
class DBAPITC(CubicWebTC):
def test_public_repo_api(self):
@@ -82,6 +83,20 @@
req.ajax_replace_url('domid') # don't crash
req.user.cw_adapt_to('IBreadCrumbs') # don't crash
+ def test_call_service(self):
+ ServiceClass = self.vreg['services']['test_service'][0]
+ for _cw in (self.request(), self.session):
+ ret_value = _cw.call_service('test_service', msg='coucou')
+ self.assertEqual('coucou', ServiceClass.passed_here.pop())
+ self.assertEqual('babar', ret_value)
+ with self.login('anon') as ctm:
+ for _cw in (self.request(), self.session):
+ with self.assertRaises(NoSelectableObject):
+ _cw.call_service('test_service', msg='toto')
+ self.rollback()
+ self.assertEqual([], ServiceClass.passed_here)
+
+
if __name__ == '__main__':
from logilab.common.testlib import unittest_main
unittest_main()
--- a/test/unittest_entity.py Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_entity.py Tue Oct 23 15:00:53 2012 +0200
@@ -18,6 +18,8 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""unit tests for cubicweb.web.views.entities module"""
+from __future__ import with_statement
+
from datetime import datetime
from logilab.common import tempattr
@@ -28,7 +30,7 @@
from cubicweb.mttransforms import HAS_TAL
from cubicweb.entities import fetch_config
from cubicweb.uilib import soup2xhtml
-from cubicweb.schema import RQLVocabularyConstraint
+from cubicweb.schema import RQLVocabularyConstraint, RRQLExpression
class EntityTC(CubicWebTC):
@@ -361,6 +363,18 @@
'NOT (S connait AD, AD nom "toto"), AD is Personne, '
'EXISTS(S travaille AE, AE nom "tutu")')
+ def test_unrelated_rql_security_rel_perms(self):
+ '''check `connait` add permission has no effect for a new entity on the
+ unrelated rql'''
+ rdef = self.schema['Personne'].rdef('connait')
+ perm_rrqle = RRQLExpression('U has_update_permission S')
+ with self.temporary_permissions((rdef, {'add': (perm_rrqle,)})):
+ person = self.vreg['etypes'].etype_class('Personne')(self.request())
+ rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
+ self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
+ 'O is Personne, O nom AA, O prenom AB, '
+ 'O modification_date AC')
+
def test_unrelated_rql_constraints_edition_subject(self):
person = self.request().create_entity('Personne', nom=u'sylvain')
rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0]
@@ -673,37 +687,37 @@
# ambiguity test
person2 = req.create_entity('Personne', prenom=u'remi', nom=u'doe')
person.cw_clear_all_caches()
- self.assertEqual(person.rest_path(), 'personne/eid/%s' % person.eid)
- self.assertEqual(person2.rest_path(), 'personne/eid/%s' % person2.eid)
+ self.assertEqual(person.rest_path(), unicode(person.eid))
+ self.assertEqual(person2.rest_path(), unicode(person2.eid))
# unique attr with None value (wikiid in this case)
card1 = req.create_entity('Card', title=u'hop')
- self.assertEqual(card1.rest_path(), 'card/eid/%s' % card1.eid)
+ self.assertEqual(card1.rest_path(), unicode(card1.eid))
# don't use rest if we have /, ? or & in the path (breaks mod_proxy)
card2 = req.create_entity('Card', title=u'pod', wikiid=u'zo/bi')
- self.assertEqual(card2.rest_path(), 'card/eid/%d' % card2.eid)
+ self.assertEqual(card2.rest_path(), unicode(card2.eid))
card3 = req.create_entity('Card', title=u'pod', wikiid=u'zo&bi')
- self.assertEqual(card3.rest_path(), 'card/eid/%d' % card3.eid)
+ self.assertEqual(card3.rest_path(), unicode(card3.eid))
card4 = req.create_entity('Card', title=u'pod', wikiid=u'zo?bi')
- self.assertEqual(card4.rest_path(), 'card/eid/%d' % card4.eid)
+ self.assertEqual(card4.rest_path(), unicode(card4.eid))
- def test_set_attributes(self):
+ def test_cw_set_attributes(self):
req = self.request()
person = req.create_entity('Personne', nom=u'di mascio', prenom=u'adrien')
self.assertEqual(person.prenom, u'adrien')
self.assertEqual(person.nom, u'di mascio')
- person.set_attributes(prenom=u'sylvain', nom=u'thénault')
+ person.cw_set(prenom=u'sylvain', nom=u'thénault')
person = self.execute('Personne P').get_entity(0, 0) # XXX retreival needed ?
self.assertEqual(person.prenom, u'sylvain')
self.assertEqual(person.nom, u'thénault')
- def test_set_relations(self):
+ def test_cw_set_relations(self):
req = self.request()
person = req.create_entity('Personne', nom=u'chauvat', prenom=u'nicolas')
note = req.create_entity('Note', type=u'x')
- note.set_relations(ecrit_par=person)
+ note.cw_set(ecrit_par=person)
note = req.create_entity('Note', type=u'y')
- note.set_relations(ecrit_par=person.eid)
+ note.cw_set(ecrit_par=person.eid)
self.assertEqual(len(person.reverse_ecrit_par), 2)
def test_metainformation_and_external_absolute_url(self):
@@ -723,7 +737,7 @@
req = self.request()
card = req.create_entity('Card', wikiid=u'', title=u'test')
self.assertEqual(card.absolute_url(),
- 'http://testing.fr/cubicweb/card/eid/%s' % card.eid)
+ 'http://testing.fr/cubicweb/%s' % card.eid)
def test_create_entity(self):
req = self.request()
--- a/test/unittest_migration.py Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_migration.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -108,7 +108,13 @@
self.assertEqual(source['db-driver'], 'sqlite')
handler = get_test_db_handler(config)
handler.init_test_database()
-
+ handler.build_db_cache()
+ repo, cnx = handler.get_repo_and_cnx()
+ cu = cnx.cursor()
+ self.assertEqual(cu.execute('Any SN WHERE X is CWUser, X login "admin", X in_state S, S name SN').rows,
+ [['activated']])
+ cnx.close()
+ repo.shutdown()
if __name__ == '__main__':
unittest_main()
--- a/test/unittest_req.py Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_req.py Tue Oct 23 15:00:53 2012 +0200
@@ -36,7 +36,7 @@
req = RequestSessionBase(None)
req.from_controller = lambda : 'view'
req.relative_path = lambda includeparams=True: None
- req.base_url = lambda : 'http://testing.fr/cubicweb/'
+ req.base_url = lambda secure=None: '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')
--- a/test/unittest_rqlrewrite.py Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_rqlrewrite.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -183,9 +183,9 @@
self.assertEqual(rqlst.as_string(),
"Any A,C,T WHERE A documented_by C?, A is Affaire "
"WITH C,T BEING (Any C,T WHERE C title T, "
- "EXISTS(C in_state B, D in_group F, G require_state B, G name 'read', G require_group F), "
- "D eid %(A)s, C is Card, "
- "EXISTS(C in_state E, E name 'public'))")
+ "(EXISTS(C in_state B, D in_group F, G require_state B, G name 'read', G require_group F)) "
+ "OR (EXISTS(C in_state E, E name 'public')), "
+ "D eid %(A)s, C is Card)")
def test_optional_var_4(self):
constraint1 = 'A created_by U, X documented_by A'
@@ -199,8 +199,8 @@
u'Any X,LA,Y WHERE LA? documented_by X, LA concerne Y, B eid %(C)s, '
'EXISTS(X created_by B), EXISTS(Y created_by B), '
'X is Card, Y is IN(Division, Note, Societe) '
- 'WITH LA BEING (Any LA WHERE EXISTS(A created_by B, LA documented_by A), '
- 'B eid %(D)s, LA is Affaire, EXISTS(E created_by B, LA concerne E))')
+ 'WITH LA BEING (Any LA WHERE (EXISTS(A created_by B, LA documented_by A)) OR (EXISTS(E created_by B, LA concerne E)), '
+ 'B eid %(D)s, LA is Affaire)')
def test_optional_var_inlined(self):
c1 = ('X require_permission P')
@@ -431,6 +431,33 @@
self.assertEqual(rqlst.as_string(),
u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire')
+ def test_rqlexpr_multiexpr_outerjoin(self):
+ c1 = RRQLExpression('X owned_by Z, Z login "hop"', 'X')
+ c2 = RRQLExpression('X owned_by Z, Z login "hip"', 'X')
+ c3 = RRQLExpression('X owned_by Z, Z login "momo"', 'X')
+ rqlst = rqlhelper.parse('Any A WHERE A documented_by C?', annotate=False)
+ rewrite(rqlst, {('C', 'X'): (c1, c2, c3)}, {}, 'X')
+ self.assertEqual(rqlst.as_string(),
+ u'Any A WHERE A documented_by C?, A is Affaire '
+ 'WITH C BEING (Any C WHERE ((EXISTS(C owned_by B, B login "hop")) '
+ 'OR (EXISTS(C owned_by D, D login "momo"))) '
+ 'OR (EXISTS(C owned_by A, A login "hip")), C is Card)')
+
+ def test_multiple_erql_one_bad(self):
+ #: reproduce bug #2236985
+ #: (rqlrewrite fails to remove rewritten entry for unsupported constraint and then crash)
+ #:
+ #: This check a very rare code path triggered by the four condition below
+
+ # 1. c_ok introduce an ambiguity
+ c_ok = ERQLExpression('X concerne R')
+ # 2. c_bad is just plain wrong and won't be kept
+ # 3. but it declare a new variable
+ # 4. this variable require a rewrite
+ c_bad = ERQLExpression('X documented_by R, A in_state R')
+
+ rqlst = parse('Any A, R WHERE A ref R, S is Affaire')
+ rewrite(rqlst, {('A', 'X'): (c_ok, c_bad)}, {})
if __name__ == '__main__':
unittest_main()
--- a/test/unittest_schema.py Wed Feb 22 11:57:42 2012 +0100
+++ b/test/unittest_schema.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -348,6 +348,10 @@
self.assertEqual(cstr.repo_check(self.session, 1, self.session.user.eid),
None) # no validation error, constraint checked
+class WorkflowShemaTC(CubicWebTC):
+ def test_trinfo_default_format(self):
+ tr = self.request().user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
+ self.assertEqual(tr.comment_format, 'text/plain')
if __name__ == '__main__':
unittest_main()
--- a/transaction.py Wed Feb 22 11:57:42 2012 +0100
+++ b/transaction.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 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 @@
This module is in the cubicweb package and not in cubicweb.server because those
objects should be accessible to client through pyro, where the cubicweb.server
package may not be installed.
-
"""
__docformat__ = "restructuredtext en"
_ = unicode
@@ -39,8 +38,12 @@
class NoSuchTransaction(RepositoryError):
- pass
+ # Used by CubicWebException
+ msg = _("there is no transaction #%s")
+ def __init__(self, txuuid):
+ super(RepositoryError, self).__init__(txuuid)
+ self.txuuid = txuuid
class Transaction(object):
"""an undoable transaction"""
@@ -82,6 +85,11 @@
def label(self):
return ACTION_LABELS[self.action]
+ @property
+ def ertype(self):
+ """ Return the entity or relation type this action is related to"""
+ raise NotImplementedError(self)
+
class EntityAction(AbstractAction):
def __init__(self, action, public, order, etype, eid, changes):
@@ -95,6 +103,11 @@
self.label, self.eid, self.changes,
self.public and 'dbapi' or 'hook')
+ @property
+ def ertype(self):
+ """ Return the entity or relation type this action is related to"""
+ return self.etype
+
class RelationAction(AbstractAction):
def __init__(self, action, public, order, rtype, eidfrom, eidto):
@@ -107,3 +120,8 @@
return '<%s: %s %s %s (%s)>' % (
self.label, self.eid_from, self.rtype, self.eid_to,
self.public and 'dbapi' or 'hook')
+
+ @property
+ def ertype(self):
+ """ Return the entity or relation type this action is related to"""
+ return self.rtype
--- a/uilib.py Wed Feb 22 11:57:42 2012 +0100
+++ b/uilib.py Tue Oct 23 15:00:53 2012 +0200
@@ -108,7 +108,7 @@
elif value.days > 2 or value.days < -2:
return req._('%d days') % int(value.days)
else:
- minus = 1 if value.days > 0 else -1
+ minus = 1 if value.days >= 0 else -1
if value.seconds > 3600:
return req._('%d hours') % (int(value.seconds // 3600) * minus)
elif value.seconds >= 120:
--- a/view.py Wed Feb 22 11:57:42 2012 +0100
+++ b/view.py Tue Oct 23 15:00:53 2012 +0200
@@ -90,19 +90,30 @@
# base view object ############################################################
class View(AppObject):
- """abstract view class, used as base for every renderable object such
- as views, templates, some components...web
+ """This class is an abstraction of a view class, used as a base class for
+ every renderable object such as views, templates and other user interface
+ components.
- A view is instantiated to render a [part of a] result set. View
- subclasses may be parametred using the following class attributes:
+ A `View` is instantiated to render a result set or part of a result
+ set. `View` subclasses may be parametrized using the following class
+ attributes:
- * `templatable` indicates if the view may be embeded in a main
- template or if it has to be rendered standalone (i.e. XML for
- instance)
- * if the view is not templatable, it should set the `content_type` class
- attribute to the correct MIME type (text/xhtml by default)
- * the `category` attribute may be used in the interface to regroup related
- objects together
+ :py:attr:`templatable` indicates if the view may be embedded in a main
+ template or if it has to be rendered standalone (i.e. pure XML views must
+ not be embedded in the main template of HTML pages)
+ :py:attr:`content_type` if the view is not templatable, it should set the
+ `content_type` class attribute to the correct MIME type (text/xhtml being
+ the default)
+ :py:attr:`category` this attribute may be used in the interface to regroup
+ related objects (view kinds) together
+
+ :py:attr:`paginable`
+
+ :py:attr:`binary`
+
+
+ A view writes to its output stream thanks to its attribute `w` (the
+ append method of an `UStreamIO`, except for binary views).
At instantiation time, the standard `_cw`, and `cw_rset` attributes are
added and the `w` attribute will be set at rendering time to a write
--- a/web/_exceptions.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/_exceptions.py Tue Oct 23 15:00:53 2012 +0200
@@ -20,59 +20,90 @@
__docformat__ = "restructuredtext en"
+import httplib
+
from cubicweb._exceptions import *
from cubicweb.utils import json_dumps
+
+class DirectResponse(Exception):
+ """Used to supply a twitted HTTP Response directly"""
+ def __init__(self, response):
+ self.response = response
+
+class InvalidSession(CubicWebException):
+ """raised when a session id is found but associated session is not found or
+ invalid"""
+
+# Publish related exception
+
class PublishException(CubicWebException):
"""base class for publishing related exception"""
+ def __init__(self, *args, **kwargs):
+ self.status = kwargs.pop('status', httplib.OK)
+ super(PublishException, self).__init__(*args, **kwargs)
+
+class LogOut(PublishException):
+ """raised to ask for deauthentication of a logged in user"""
+ def __init__(self, url=None):
+ super(LogOut, self).__init__()
+ self.url = url
+
+class Redirect(PublishException):
+ """raised to redirect the http request"""
+ def __init__(self, location, status=httplib.SEE_OTHER):
+ super(Redirect, self).__init__(status=status)
+ self.location = location
+
+class StatusResponse(PublishException):
+
+ def __init__(self, status, content=''):
+ super(StatusResponse, self).__init__(status=status)
+ self.content = content
+
+ def __repr__(self):
+ return '%s(%r, %r)' % (self.__class__.__name__, self.status, self.content)
+ self.url = url
+
+# Publish related error
+
class RequestError(PublishException):
"""raised when a request can't be served because of a bad input"""
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('status', httplib.BAD_REQUEST)
+ super(RequestError, self).__init__(*args, **kwargs)
+
+
class NothingToEdit(RequestError):
"""raised when an edit request doesn't specify any eid to edit"""
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('status', httplib.BAD_REQUEST)
+ super(NothingToEdit, self).__init__(*args, **kwargs)
+
class ProcessFormError(RequestError):
"""raised when posted data can't be processed by the corresponding field
"""
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('status', httplib.BAD_REQUEST)
+ super(ProcessFormError, self).__init__(*args, **kwargs)
class NotFound(RequestError):
- """raised when a 404 error should be returned"""
-
-class Redirect(PublishException):
- """raised to redirect the http request"""
- def __init__(self, location):
- self.location = location
-
-class DirectResponse(Exception):
- def __init__(self, response):
- self.response = response
+ """raised when something was not found. In most case,
+ a 404 error should be returned"""
-class StatusResponse(Exception):
- def __init__(self, status, content=''):
- self.status = int(status)
- self.content = content
-
- def __repr__(self):
- return '%s(%r, %r)' % (self.__class__.__name__, self.status, self.content)
-
-class InvalidSession(CubicWebException):
- """raised when a session id is found but associated session is not found or
- invalid
- """
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('status', httplib.NOT_FOUND)
+ super(NotFound, self).__init__(*args, **kwargs)
class RemoteCallFailed(RequestError):
"""raised when a json remote call fails
"""
- def __init__(self, reason=''):
- super(RemoteCallFailed, self).__init__()
+ def __init__(self, reason='', status=httplib.INTERNAL_SERVER_ERROR):
+ super(RemoteCallFailed, self).__init__(status=status)
self.reason = reason
def dumps(self):
return json_dumps({'reason': self.reason})
-
-class LogOut(PublishException):
- """raised to ask for deauthentication of a logged in user"""
- def __init__(self, url):
- super(LogOut, self).__init__()
- self.url = url
--- a/web/application.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/application.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -24,6 +24,9 @@
import sys
from time import clock, time
from contextlib import contextmanager
+from warnings import warn
+
+import httplib
from logilab.common.deprecation import deprecated
@@ -39,6 +42,8 @@
StatusResponse, DirectResponse, Redirect, NotFound, LogOut,
RemoteCallFailed, InvalidSession, RequestError)
+from cubicweb.web.request import CubicWebRequestBase
+
# make session manager available through a global variable so the debug view can
# print information about web session
SESSION_MANAGER = None
@@ -288,11 +293,11 @@
if config['query-log-file']:
from threading import Lock
self._query_log = open(config['query-log-file'], 'a')
- self.publish = self.log_publish
+ self.handle_request = self.log_handle_request
self._logfile_lock = Lock()
else:
self._query_log = None
- self.publish = self.main_publish
+ self.handle_request = self.main_handle_request
# instantiate session and url resolving helpers
self.session_handler = session_handler_fact(self)
self.set_urlresolver()
@@ -311,12 +316,12 @@
# publish methods #########################################################
- def log_publish(self, path, req):
+ def log_handle_request(self, req, path):
"""wrapper around _publish to log all queries executed for a given
accessed path
"""
try:
- return self.main_publish(path, req)
+ return self.main_handle_request(req, path)
finally:
cnx = req.cnx
if cnx:
@@ -332,7 +337,79 @@
except Exception:
self.exception('error while logging queries')
- def main_publish(self, path, req):
+
+
+ def main_handle_request(self, req, path):
+ if not isinstance(req, CubicWebRequestBase):
+ warn('[3.15] Application entry poin arguments are now (req, path) '
+ 'not (path, req)', DeprecationWarning, 2)
+ req, path = path, req
+ if req.authmode == 'http':
+ # activate realm-based auth
+ realm = self.vreg.config['realm']
+ req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
+ content = ''
+ try:
+ self.connect(req)
+ # DENY https acces for anonymous_user
+ if (req.https
+ and req.session.anonymous_session
+ and self.vreg.config['https-deny-anonymous']):
+ # don't allow anonymous on https connection
+ raise AuthenticationError()
+ # nested try to allow LogOut to delegate logic to AuthenticationError
+ # handler
+ try:
+ ### Try to generate the actual request content
+ content = self.core_handle(req, path)
+ # Handle user log-out
+ except LogOut, ex:
+ # When authentification is handled by cookie the code that
+ # raised LogOut must has invalidated the cookie. We can just
+ # reload the original url without authentification
+ if self.vreg.config['auth-mode'] == 'cookie' and ex.url:
+ req.headers_out.setHeader('location', str(ex.url))
+ if ex.status is not None:
+ req.status_out = httplib.SEE_OTHER
+ # When the authentification is handled by http we must
+ # explicitly ask for authentification to flush current http
+ # authentification information
+ else:
+ # Render "logged out" content.
+ # assignement to ``content`` prevent standard
+ # AuthenticationError code to overwrite it.
+ content = self.loggedout_content(req)
+ # let the explicitly reset http credential
+ raise AuthenticationError()
+ except Redirect, ex:
+ # authentication needs redirection (eg openid)
+ content = self.redirect_handler(req, ex)
+ # Wrong, absent or Reseted credential
+ except AuthenticationError:
+ # If there is an https url configured and
+ # the request do not used https, redirect to login form
+ https_url = self.vreg.config['https-url']
+ if https_url and req.base_url() != https_url:
+ req.status_out = httplib.SEE_OTHER
+ req.headers_out.setHeader('location', https_url + 'login')
+ 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 = httplib.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 = httplib.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)
+ return content
+
+ def core_handle(self, req, path):
"""method called by the main publisher to process <path>
should return a string containing the resulting page or raise a
@@ -347,89 +424,93 @@
:rtype: str
:return: the result of the pusblished url
"""
- path = path or 'view'
# don't log form values they may contains sensitive information
- self.info('publish "%s" (%s, form params: %s)',
- path, req.session.sessionid, req.form.keys())
+ self.debug('publish "%s" (%s, form params: %s)',
+ path, req.session.sessionid, req.form.keys())
# remove user callbacks on a new request (except for json controllers
# to avoid callbacks being unregistered before they could be called)
tstart = clock()
commited = False
try:
+ ### standard processing of the request
try:
ctrlid, rset = self.url_resolver.process(req, path)
try:
controller = self.vreg['controllers'].select(ctrlid, req,
appli=self)
except NoSelectableObject:
- if ctrlid == 'login':
- raise Unauthorized(req._('log out first'))
raise Unauthorized(req._('not authorized'))
req.update_search_state()
result = controller.publish(rset=rset)
- if req.cnx:
- # no req.cnx if anonymous aren't allowed and we are
- # displaying some anonymous enabled view such as the cookie
- # authentication form
- req.cnx.commit()
- commited = True
- except (StatusResponse, DirectResponse):
- if req.cnx:
- req.cnx.commit()
- raise
- except (AuthenticationError, LogOut):
- raise
- except Redirect:
- # redirect is raised by edit controller when everything went fine,
- # so try to commit
- try:
- if req.cnx:
- txuuid = req.cnx.commit()
- if txuuid is not None:
- msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
- req.build_url('undo', txuuid=txuuid), req._('undo'))
- req.append_to_redirect_message(msg)
- except ValidationError, ex:
- self.validation_error_handler(req, ex)
- except Unauthorized, ex:
- req.data['errmsg'] = req._('You\'re not authorized to access this page. '
- 'If you think you should, please contact the site administrator.')
- self.error_handler(req, ex, tb=False)
- except Exception, ex:
- self.error_handler(req, ex, tb=True)
- else:
- # delete validation errors which may have been previously set
- if '__errorurl' in req.form:
- req.session.data.pop(req.form['__errorurl'], None)
- raise
- except RemoteCallFailed, ex:
- req.set_header('content-type', 'application/json')
- raise StatusResponse(500, ex.dumps())
- except NotFound:
- raise StatusResponse(404, self.notfound_content(req))
- except ValidationError, ex:
- self.validation_error_handler(req, ex)
- except Unauthorized, ex:
- self.error_handler(req, ex, tb=False, code=403)
- except (BadRQLQuery, RequestError), ex:
- self.error_handler(req, ex, tb=False)
- except BaseException, ex:
- self.error_handler(req, ex, tb=True)
- except:
- self.critical('Catch all triggered!!!')
- self.exception('this is what happened')
- result = 'oops'
+ except StatusResponse, ex:
+ warn('StatusResponse is deprecated use req.status_out',
+ DeprecationWarning)
+ result = ex.content
+ req.status_out = ex.status
+ except Redirect, ex:
+ # Redirect may be raised by edit controller when everything went
+ # fine, so attempt to commit
+ result = self.redirect_handler(req, ex)
+ if req.cnx:
+ txuuid = req.cnx.commit()
+ commited = True
+ if txuuid is not None:
+ req.data['last_undoable_transaction'] = txuuid
+ ### error case
+ except NotFound, ex:
+ result = self.notfound_content(req)
+ req.status_out = ex.status
+ except ValidationError, ex:
+ result = self.validation_error_handler(req, ex)
+ except RemoteCallFailed, ex:
+ result = self.ajax_error_handler(req, ex)
+ except Unauthorized, ex:
+ req.data['errmsg'] = req._('You\'re not authorized to access this page. '
+ 'If you think you should, please contact the site administrator.')
+ req.status_out = httplib.UNAUTHORIZED
+ result = self.error_handler(req, ex, tb=False)
+ except (BadRQLQuery, RequestError), ex:
+ result = self.error_handler(req, ex, tb=False)
+ ### pass through exception
+ except DirectResponse:
+ if req.cnx:
+ req.cnx.commit()
+ raise
+ except (AuthenticationError, LogOut):
+ # the rollback is handled in the finally
+ raise
+ ### Last defense line
+ except BaseException, ex:
+ result = self.error_handler(req, ex, tb=True)
finally:
if req.cnx and not commited:
try:
req.cnx.rollback()
except Exception:
pass # ignore rollback error at this point
- self.info('query %s executed in %s sec', req.relative_path(), clock() - tstart)
+ # request may be referenced by "onetime callback", so clear its entity
+ # cache to avoid memory usage
+ req.drop_entity_cache()
+ self.add_undo_link_to_msg(req)
+ self.debug('query %s executed in %s sec', req.relative_path(), clock() - tstart)
return result
+ # Error handlers
+
+ def redirect_handler(self, req, ex):
+ """handle redirect
+ - comply to ex status
+ - set header field
+ - return empty content
+ """
+ self.debug('redirecting to %s', str(ex.location))
+ req.headers_out.setHeader('location', str(ex.location))
+ assert 300 <= ex.status < 400
+ req.status_out = ex.status
+ return ''
+
def validation_error_handler(self, req, ex):
- ex.errors = dict((k, v) for k, v in ex.errors.items())
+ ex.tr(req._) # translate messages using ui language
if '__errorurl' in req.form:
forminfo = {'error': ex,
'values': req.form,
@@ -440,18 +521,23 @@
# session key is 'url + #<form dom id', though we usually don't want
# the browser to move to the form since it hides the global
# messages.
- raise Redirect(req.form['__errorurl'].rsplit('#', 1)[0])
- self.error_handler(req, ex, tb=False)
+ location = req.form['__errorurl'].rsplit('#', 1)[0]
+ req.headers_out.setHeader('location', str(location))
+ req.status_out = httplib.SEE_OTHER
+ return ''
+ req.status_out = httplib.CONFLICT
+ return self.error_handler(req, ex, tb=False)
- def error_handler(self, req, ex, tb=False, code=500):
+ def error_handler(self, req, ex, tb=False):
excinfo = sys.exc_info()
- self.exception(repr(ex))
+ if tb:
+ self.exception(repr(ex))
req.set_header('Cache-Control', 'no-cache')
req.remove_header('Etag')
req.reset_message()
req.reset_headers()
if req.ajax_request:
- raise RemoteCallFailed(unicode(ex))
+ return ajax_error_handler(req, ex)
try:
req.data['ex'] = ex
if tb:
@@ -462,7 +548,27 @@
content = self.vreg['views'].main_template(req, template, view=errview)
except Exception:
content = self.vreg['views'].main_template(req, 'error-template')
- raise StatusResponse(code, content)
+ if getattr(ex, 'status', None) is not None:
+ req.status_out = ex.status
+ return content
+
+ def add_undo_link_to_msg(self, req):
+ txuuid = req.data.get('last_undoable_transaction')
+ if txuuid is not None:
+ msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
+ req.build_url('undo', txuuid=txuuid), req._('undo'))
+ req.append_to_redirect_message(msg)
+
+ def ajax_error_handler(self, req, ex):
+ req.set_header('content-type', 'application/json')
+ status = ex.status
+ if status is None:
+ status = httplib.INTERNAL_SERVER_ERROR
+ json_dumper = getattr(ex, 'dumps', lambda : unicode(ex))
+ req.status_out = status
+ return json_dumper()
+
+ # special case handling
def need_login_content(self, req):
return self.vreg['views'].main_template(req, 'login')
@@ -476,6 +582,8 @@
template = self.main_template_id(req)
return self.vreg['views'].main_template(req, template, view=view)
+ # template stuff
+
def main_template_id(self, req):
template = req.form.get('__template', req.property_value('ui.main-template'))
if template not in self.vreg['views']:
--- a/web/data/cubicweb.ajax.box.js Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.ajax.box.js Tue Oct 23 15:00:53 2012 +0200
@@ -13,11 +13,11 @@
if (separator) {
value = $.map(value.split(separator), jQuery.trim);
}
- var d = loadRemote('json', ajaxFuncArgs(fname, null, eid, value));
+ var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs(fname, null, eid, value));
d.addCallback(function() {
$('#' + holderid).empty();
var formparams = ajaxFuncArgs('render', null, 'ctxcomponents', boxid, eid);
- $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+ $('#' + cw.utils.domid(boxid) + eid).loadxhtml(AJAX_BASE_URL, formparams);
if (msg) {
document.location.hash = '#header';
updateMessage(msg);
@@ -26,10 +26,10 @@
}
function ajaxBoxRemoveLinkedEntity(boxid, eid, relatedeid, delfname, msg) {
- var d = loadRemote('json', ajaxFuncArgs(delfname, null, eid, relatedeid));
+ var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs(delfname, null, eid, relatedeid));
d.addCallback(function() {
var formparams = ajaxFuncArgs('render', null, 'ctxcomponents', boxid, eid);
- $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+ $('#' + cw.utils.domid(boxid) + eid).loadxhtml(AJAX_BASE_URL, formparams);
if (msg) {
document.location.hash = '#header';
updateMessage(msg);
@@ -69,7 +69,7 @@
}
else {
var inputid = holderid + 'Input';
- var deferred = loadRemote('json', ajaxFuncArgs(unrelfname, null, eid));
+ var deferred = loadRemote(AJAX_BASE_URL, ajaxFuncArgs(unrelfname, null, eid));
deferred.addCallback(function (unrelated) {
var input = INPUT({'type': 'text', 'id': inputid, 'size': 20});
holder.append(input).show();
--- a/web/data/cubicweb.ajax.js Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.ajax.js Tue Oct 23 15:00:53 2012 +0200
@@ -86,8 +86,9 @@
});
-
+var AJAX_PREFIX_URL = 'ajax';
var JSON_BASE_URL = baseuri() + 'json?';
+var AJAX_BASE_URL = baseuri() + AJAX_PREFIX_URL + '?';
jQuery.extend(cw.ajax, {
@@ -180,8 +181,17 @@
// compute concat-like url for missing resources and append <link>
// element to $head
if (missingStylesheetsUrl) {
- $srcnode.attr('href', missingStylesheetsUrl);
- $srcnode.appendTo($head);
+ // IE has problems with dynamic CSS insertions. One symptom (among others)
+ // is a "1 item remaining" message in the status bar. (cf. #2356261)
+ // document.createStyleSheet needs to be used for this, although it seems
+ // that IE can't create more than 31 additional stylesheets with
+ // document.createStyleSheet.
+ if ($.browser.msie) {
+ document.createStyleSheet(missingStylesheetsUrl);
+ } else {
+ $srcnode.attr('href', missingStylesheetsUrl);
+ $srcnode.appendTo($head);
+ }
}
}
});
@@ -371,7 +381,7 @@
/**
* .. function:: loadRemote(url, form, reqtype='GET', sync=false)
*
- * Asynchronously (unless `async` argument is set to false) load an url or path
+ * Asynchronously (unless `sync` argument is set to true) load an url or path
* and return a deferred whose callbacks args are decoded according to the
* Content-Type response header. `form` should be additional form params
* dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
@@ -439,7 +449,7 @@
* emulation of gettext's _ shortcut
*/
function _(message) {
- return loadRemote('json', ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
+ return loadRemote(AJAX_BASE_URL, ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
}
/**
@@ -495,19 +505,19 @@
}
extraparams['rql'] = rql;
extraparams['vid'] = vid;
- $fragment.loadxhtml('json', ajaxFuncArgs('view', extraparams));
+ $fragment.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('view', extraparams));
}
}
function unloadPageData() {
// NOTE: do not make async calls on unload if you want to avoid
// strange bugs
- loadRemote('json', ajaxFuncArgs('unload_page_data'), 'GET', true);
+ loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unload_page_data'), 'GET', true);
}
function removeBookmark(beid) {
- var d = loadRemote('json', ajaxFuncArgs('delete_bookmark', null, beid));
+ var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('delete_bookmark', null, beid));
d.addCallback(function(boxcontent) {
- $('#bookmarks_box').loadxhtml('json',
+ $('#bookmarks_box').loadxhtml(AJAX_BASE_URL,
ajaxFuncArgs('render', null, 'ctxcomponents',
'bookmarks_box'));
document.location.hash = '#header';
@@ -517,7 +527,7 @@
function userCallback(cbname) {
setProgressCursor();
- var d = loadRemote('json', ajaxFuncArgs('user_callback', null, cbname));
+ var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('user_callback', null, cbname));
d.addCallback(resetCursor);
d.addErrback(resetCursor);
d.addErrback(remoteCallFailed);
@@ -527,7 +537,7 @@
function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
var d = userCallback(cbname);
d.addCallback(function() {
- $('#' + nodeid).loadxhtml('json', ajaxFuncArgs('render', {'rql': rql},
+ $('#' + nodeid).loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {'rql': rql},
registry, compid));
if (msg) {
updateMessage(msg);
@@ -553,7 +563,7 @@
*/
function unregisterUserCallback(cbname) {
setProgressCursor();
- var d = loadRemote('json', ajaxFuncArgs('unregister_user_callback',
+ var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unregister_user_callback',
null, cbname));
d.addCallback(resetCursor);
d.addErrback(resetCursor);
@@ -679,7 +689,7 @@
var compid = this.id.replace("_", ".").rstrip(creationEid);
var params = ajaxFuncArgs('render', null, 'ctxcomponents',
compid, actualEid);
- $(this).loadxhtml('json', params, null, 'swap', true);
+ $(this).loadxhtml(AJAX_BASE_URL, params, null, 'swap', true);
});
$compsholder.attr('id', context + actualEid);
}
@@ -694,7 +704,7 @@
var ajaxArgs = ['render', formparams, registry, compid];
ajaxArgs = ajaxArgs.concat(cw.utils.sliceList(arguments, 4));
var params = ajaxFuncArgs.apply(null, ajaxArgs);
- return $('#'+domid).loadxhtml('json', params, null, 'swap');
+ return $('#'+domid).loadxhtml(AJAX_BASE_URL, params, null, 'swap', true);
}
/* ajax tabs ******************************************************************/
@@ -738,8 +748,8 @@
nodeid = nodeid || (compid + 'Component');
extraargs = extraargs || {};
var node = cw.jqNode(nodeid);
- return node.loadxhtml('json', ajaxFuncArgs('component', null, compid,
- rql, registry, extraargs));
+ return node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('component', null, compid,
+ rql, registry, extraargs));
}
);
@@ -775,7 +785,7 @@
// passing `props` directly to loadxml because replacePageChunk
// is sometimes called (abusively) with some extra parameters in `vid`
var mode = swap ? 'swap': 'replace';
- var url = JSON_BASE_URL + asURL(props);
+ var url = AJAX_BASE_URL + asURL(props);
jQuery(node).loadxhtml(url, params, 'get', mode);
} else {
cw.log('Node', nodeId, 'not found');
@@ -798,7 +808,7 @@
arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
};
var result = jQuery.ajax({
- url: JSON_BASE_URL,
+ url: AJAX_BASE_URL,
data: props,
async: false,
traditional: true
@@ -818,7 +828,7 @@
arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
};
// XXX we should inline the content of loadRemote here
- var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
+ var deferred = loadRemote(AJAX_BASE_URL, props, 'POST');
deferred = deferred.addErrback(remoteCallFailed);
deferred = deferred.addErrback(resetCursor);
deferred = deferred.addCallback(resetCursor);
--- a/web/data/cubicweb.css Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.css Tue Oct 23 15:00:53 2012 +0200
@@ -39,6 +39,12 @@
padding: %(h3Padding)s;
}
+
+h4 {
+ font-size: %(h4FontSize)s;
+}
+
+
div.tabbedprimary + h1,
h1.plain {
border-bottom: none;
@@ -214,9 +220,6 @@
font-style: italic;
}
-.align-center{
- text-align: center;
-}
/***************************************/
/* LAYOUT */
@@ -239,13 +242,21 @@
table#header td#header-right {
padding-top: 1em;
- float: right;
+ white-space: nowrap;
}
table#header img#logo{
vertical-align: middle;
}
+table#header td#headtext {
+ white-space: nowrap;
+}
+
+table#header td#header-center{
+ width: 100%;
+}
+
span#appliName {
font-weight: bold;
color: %(defaultColor)s;
@@ -534,6 +545,16 @@
padding-left: 2em;
}
+/* actions around tables */
+.tableactions span {
+ padding: 0 18px;
+ height: 24px;
+ background: #F8F8F8;
+ border: 1px solid #DFDFDF;
+ border-bottom: none;
+ border-radius: 4px 4px 0 0;
+}
+
/* custom boxes */
.search_box div.boxBody {
@@ -978,11 +999,20 @@
/********************************/
img.align-right {
- margin-left: 1.5em;
+ margin-left: auto;
+ display:block;
}
img.align-left {
- margin-right: 1.5em;
+ margin-right: auto;
+ display:block;
+}
+
+img.align-center{
+ text-align: center;
+ margin-left: auto;
+ margin-right: auto;
+ display:block;
}
/******************************/
--- a/web/data/cubicweb.edition.js Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.edition.js Tue Oct 23 15:00:53 2012 +0200
@@ -28,7 +28,7 @@
pageid: pageid,
arg: $.map([key, varname, tabindex], jQuery.toJSON)
};
- cw.jqNode('div:value:' + varname).loadxhtml(JSON_BASE_URL, args, 'post');
+ cw.jqNode('div:value:' + varname).loadxhtml(AJAX_BASE_URL, args, 'post');
}
}
@@ -170,8 +170,8 @@
// add hidden parameter
var entityForm = jQuery('#entityForm');
var oid = optionNode.id.substring(2); // option id is prefixed by "id"
- loadRemote('json', ajaxFuncArgs('add_pending_inserts', null,
- [oid.split(':')]), 'GET', true);
+ loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_inserts', null,
+ [oid.split(':')]), 'GET', true);
var selectNode = optionNode.parentNode;
// remove option node
selectNode.removeChild(optionNode);
@@ -209,8 +209,8 @@
}
}
elementId = elementId.substring(2, elementId.length);
- loadRemote('json', ajaxFuncArgs('remove_pending_insert', null,
- elementId.split(':')), 'GET', true);
+ loadRemote(AJAX_BASE_URL, ajaxFuncArgs('remove_pending_insert', null,
+ elementId.split(':')), 'GET', true);
}
/**
@@ -234,7 +234,7 @@
* * `nodeId`, eid_from:r_type:eid_to
*/
function addPendingDelete(nodeId, eid) {
- var d = loadRemote('json', ajaxFuncArgs('add_pending_delete', null, nodeId.split(':')));
+ var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_delete', null, nodeId.split(':')));
d.addCallback(function() {
// and strike entity view
cw.jqNode('span' + nodeId).addClass('pendingDelete');
@@ -249,7 +249,7 @@
* * `nodeId`, eid_from:r_type:eid_to
*/
function cancelPendingDelete(nodeId, eid) {
- var d = loadRemote('json', ajaxFuncArgs('remove_pending_delete', null, nodeId.split(':')));
+ var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('remove_pending_delete', null, nodeId.split(':')));
d.addCallback(function() {
// reset link's CSS class
cw.jqNode('span' + nodeId).removeClass('pendingDelete');
@@ -275,7 +275,7 @@
function selectForAssociation(tripletIdsString, originalEid) {
var tripletlist = $.map(tripletIdsString.split('-'),
function(x) { return [x.split(':')] ;});
- var d = loadRemote('json', ajaxFuncArgs('add_pending_inserts', null, tripletlist));
+ var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('add_pending_inserts', null, tripletlist));
d.addCallback(function() {
var args = {
vid: 'edition',
@@ -308,7 +308,7 @@
function addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore) {
insertBefore = insertBefore || cw.getNode('add' + rtype + ':' + peid + 'link').parentNode;
var args = ajaxFuncArgs('inline_creation_form', null, peid, petype, ttype, rtype, role, i18nctx);
- var d = loadRemote('json', args);
+ var d = loadRemote(AJAX_BASE_URL, args);
d.addCallback(function(response) {
var dom = getDomFromResponse(response);
loadAjaxHtmlHead(dom);
@@ -435,11 +435,15 @@
}
}
if (globalerrors.length) {
- if (globalerrors.length == 1) {
- var innernode = SPAN(null, globalerrors[0]);
- } else {
- var innernode = UL(null, $.map(globalerrors, partial(LI, null)));
- }
+ if (globalerrors.length == 1) {
+ var innernode = SPAN(null, globalerrors[0]);
+ } else {
+ var linodes =[];
+ for(var i=0; i<globalerrors.length; i++){
+ linodes.push(LI(null, globalerrors[i]));
+ }
+ var innernode = UL(null, linodes);
+ }
// insert DIV and innernode before the form
var div = DIV({
'class': "errorMessage",
@@ -587,7 +591,7 @@
try {
var zipped = cw.utils.formContents(formid);
var args = ajaxFuncArgs('validate_form', null, action, zipped[0], zipped[1]);
- var d = loadRemote('json', args, 'POST');
+ var d = loadRemote(AJAX_BASE_URL, args, 'POST');
} catch(ex) {
cw.log('got exception', ex);
return false;
--- a/web/data/cubicweb.facets.js Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.facets.js Tue Oct 23 15:00:53 2012 +0200
@@ -56,7 +56,7 @@
var zipped = facetFormContent($form);
zipped[0].push('facetargs');
zipped[1].push(vidargs);
- var d = loadRemote('json', ajaxFuncArgs('filter_build_rql', null, zipped[0], zipped[1]));
+ var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('filter_build_rql', null, zipped[0], zipped[1]));
d.addCallback(function(result) {
var rql = result[0];
var $bkLink = jQuery('#facetBkLink');
@@ -68,6 +68,14 @@
var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + encodeURIComponent(bkPath);
$bkLink.attr('href', bkUrl);
}
+ var $focusLink = jQuery('#focusLink');
+ if ($focusLink.length) {
+ var url = baseuri()+ 'view?rql=' + encodeURIComponent(rql);
+ if (vid) {
+ url += '&vid=' + encodeURIComponent(vid);
+ }
+ $focusLink.attr('href', url);
+ }
var toupdate = result[1];
var extraparams = vidargs;
if (paginate) { extraparams['paginate'] = '1'; } // XXX in vidargs
@@ -87,7 +95,7 @@
if (vid) { // XXX see copyParam above. Need cleanup
extraparams['vid'] = vid;
}
- d = $('#' + divid).loadxhtml('json', ajaxFuncArgs('view', extraparams),
+ d = $('#' + divid).loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('view', extraparams),
null, 'swap');
d.addCallback(function() {
// XXX rql/vid in extraparams
@@ -99,14 +107,14 @@
// now
var $node = jQuery('#edit_box');
if ($node.length) {
- $node.loadxhtml('json', ajaxFuncArgs('render', {
+ $node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {
'rql': rql
},
'ctxcomponents', 'edit_box'));
}
$node = jQuery('#breadcrumbs');
if ($node.length) {
- $node.loadxhtml('json', ajaxFuncArgs('render', {
+ $node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('render', {
'rql': rql
},
'ctxcomponents', 'breadcrumbs'));
@@ -118,7 +126,7 @@
mainvar = zipped[1][index];
}
- var d = loadRemote('json', ajaxFuncArgs('filter_select_content', null, toupdate, rql, mainvar));
+ var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('filter_select_content', null, toupdate, rql, mainvar));
d.addCallback(function(updateMap) {
for (facetName in updateMap) {
var values = updateMap[facetName];
--- a/web/data/cubicweb.old.css Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.old.css Tue Oct 23 15:00:53 2012 +0200
@@ -43,7 +43,7 @@
}
h4 {
- font-size: 120%;
+ font-size: %(h4FontSize)s;
margin: 0.2em 0px;
}
@@ -64,9 +64,12 @@
text-decoration: underline;
}
-a img, img {
+a img{
+ text-align: center;
+}
+
+img{
border: none;
- text-align: center;
}
img.prevnext {
@@ -161,6 +164,9 @@
color: #000;
background-color: #f2f2f2;
border: 1px solid #ccc;
+ margin: 10px 0;
+ padding-bottom: 12px;
+ padding-left: 5px;
}
code {
@@ -213,25 +219,44 @@
visibility: hidden;
}
-li.invisible { list-style: none; background: none; padding: 0px 0px
-1px 1px; }
+li.invisible {
+ list-style: none;
+ background: none;
+ padding: 0px 0px 1px 1px;
+}
li.invisible div {
display: inline;
}
.caption {
- font-weight: bold;
+ font-weight: bold;
}
.legend{
- font-style: italic;
+ font-style: italic;
+}
+
+/* rest related image classes generated with align: directive */
+
+img.align-right {
+ margin-left: auto;
+ display:block;
}
-.align-center{
- text-align: center;
+img.align-left {
+ margin-right: auto;
+ display:block;
}
+img.align-center{
+ text-align: center;
+ margin-left: auto;
+ margin-right: auto;
+ display:block;
+}
+
+
/***************************************/
/* LAYOUT */
/***************************************/
@@ -250,10 +275,16 @@
table#header a {
color: #000;
}
+table#header td#headtext {
+ white-space: nowrap;
+}
table#header td#header-right {
padding-top: 1em;
- float: right;
+ white-space: nowrap;
+}
+table#header td#header-center{
+ width: 100%;
}
span#appliName {
@@ -868,6 +899,16 @@
padding-left: 0.5em;
}
+/* actions around tables */
+.tableactions span {
+ padding: 0 18px;
+ height: 24px;
+ background: #F8F8F8;
+ border: 1px solid #DFDFDF;
+ border-bottom: none;
+ border-radius: 4px 4px 0 0;
+}
+
/***************************************/
/* error view (views/management.py) */
/***************************************/
--- a/web/data/cubicweb.reledit.js Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.reledit.js Tue Oct 23 15:00:53 2012 +0200
@@ -53,7 +53,7 @@
return;
}
}
- jQuery('#'+params.divid+'-reledit').loadxhtml(JSON_BASE_URL, params, 'post');
+ jQuery('#'+params.divid+'-reledit').loadxhtml(AJAX_BASE_URL, params, 'post');
jQuery(cw).trigger('reledit-reloaded', params);
},
@@ -69,7 +69,7 @@
pageid: pageid, action: action,
eid: eid, divid: divid, formid: formid,
reload: reload, vid: vid};
- var d = jQuery('#'+divid+'-reledit').loadxhtml(JSON_BASE_URL, args, 'post');
+ var d = jQuery('#'+divid+'-reledit').loadxhtml(AJAX_BASE_URL, args, 'post');
d.addCallback(function () {cw.reledit.showInlineEditionForm(divid);});
}
});
--- a/web/data/cubicweb.widgets.js Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/cubicweb.widgets.js Tue Oct 23 15:00:53 2012 +0200
@@ -45,11 +45,11 @@
});
function postJSON(url, data, callback) {
- return jQuery.post(url, data, callback, 'json');
+ return jQuery.post(url, data, callback, AJAX_BASE_URL);
}
function getJSON(url, data, callback) {
- return jQuery.get(url, data, callback, 'json');
+ return jQuery.get(url, data, callback, AJAX_BASE_URL);
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/fullcalendar.locale.js Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,38 @@
+/*
+ translations for fullCalendar plugin
+ */
+
+$.fullCalendar.regional = function(lng, options){
+ var defaults = {'fr' : {
+ monthNames:
+ ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'],
+ monthNamesShort: ['janv.','févr.','mars','avr.','mai','juin','juil.','août','sept.','oct.','nov.','déc.'],
+ dayNames: ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'],
+ dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
+ titleFormat: {
+ month: 'MMMM yyyy', // ex : Janvier 2010
+ week: "d[ MMMM][ yyyy]{ - d MMMM yyyy}", // ex : 10 — 16 Janvier 2010,
+ day: 'dddd d MMMM yyyy' // ex : Jeudi 14 Janvier 2010
+ },
+ columnFormat: {'month': 'dddd',
+ 'agendaWeek': 'dddd dd/M/yyyy',
+ 'agendaDay': 'dddd dd/M/yyyy'},
+ axisFormat: 'H:mm',
+ timeFormat: {
+ '': 'H:mm',
+ agenda: 'H:mm{ - H:mm}'},
+ allDayText: 'journée',
+ axisFormat: 'H:mm',
+ buttonText: {
+ today: "aujourd'hui",
+ month: 'mois',
+ week: 'semaine',
+ day: 'jour'
+ }
+ }};
+ if(lng in defaults){
+ return $.extend({}, defaults[lng], options);
+ }
+ else {return options;};
+ };
+;
\ No newline at end of file
--- a/web/data/uiprops.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/data/uiprops.py Tue Oct 23 15:00:53 2012 +0200
@@ -89,20 +89,22 @@
# h3 { font-size:1.30769em; }
# h
-h1FontSize = '1.5em' # 18px
+h1FontSize = '2.3em' # 25.3833px
h1Padding = '0 0 0.14em 0 '
h1Margin = '0.8em 0 0.5em'
h1Color = '#000'
h1BorderBottomStyle = lazystr('0.06em solid %(h1Color)s')
-h2FontSize = '1.33333em'
-h2Padding = '0.4em 0 0.35em 0'
+h2FontSize = '2em' #
+h2Padding = '0.4em 0 0.35em 0' # 22.0667px
h2Margin = '0'
-h3FontSize = '1.16667em'
+h3FontSize = '1.7em' #18.75px
h3Padding = '0.5em 0 0.57em 0'
h3Margin = '0'
+h4FontSize = '1.4em' # 15.45px
+
# links
aColor = '#e6820e'
--- a/web/form.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/form.py Tue Oct 23 15:00:53 2012 +0200
@@ -88,21 +88,19 @@
def __init__(self, req, rset=None, row=None, col=None,
submitmsg=None, mainform=True, **kwargs):
- super(Form, self).__init__(req, rset=rset, row=row, col=col)
+ # process kwargs first so we can properly pass them to Form and match
+ # order expectation (ie cw_extra_kwargs populated almost first)
+ hiddens, extrakw = self._process_kwargs(kwargs)
+ # now call ancestor init
+ super(Form, self).__init__(req, rset=rset, row=row, col=col, **extrakw)
+ # then continue with further specific initialization
self.fields = list(self.__class__._fields_)
+ for key, val in hiddens:
+ self.add_hidden(key, val)
if mainform:
- self.add_hidden(u'__form_id', kwargs.pop('formvid', self.__regid__))
- for key, val in kwargs.iteritems():
- if key in controller.NAV_FORM_PARAMETERS:
- self.add_hidden(key, val)
- elif key == 'redirect_path':
- self.add_hidden(u'__redirectpath', val)
- elif hasattr(self.__class__, key) and not key[0] == '_':
- setattr(self, key, val)
- else:
- self.cw_extra_kwargs[key] = val
- # skip other parameters, usually given for selection
- # (else write a custom class to handle them)
+ formid = kwargs.pop('formvid', self.__regid__)
+ self.add_hidden(u'__form_id', formid)
+ self._posting = self._cw.form.get('__form_id') == formid
if mainform:
self.add_hidden(u'__errorurl', self.session_key())
self.add_hidden(u'__domid', self.domid)
@@ -117,6 +115,22 @@
if submitmsg is not None:
self.set_message(submitmsg)
+ def _process_kwargs(self, kwargs):
+ hiddens = []
+ extrakw = {}
+ # search for navigation parameters and customization of existing
+ # attributes; remaining stuff goes in extrakwargs
+ for key, val in kwargs.iteritems():
+ if key in controller.NAV_FORM_PARAMETERS:
+ hiddens.append( (key, val) )
+ elif key == 'redirect_path':
+ hiddens.append( (u'__redirectpath', val) )
+ elif hasattr(self.__class__, key) and not key[0] == '_':
+ setattr(self, key, val)
+ else:
+ extrakw[key] = val
+ return hiddens, extrakw
+
def set_message(self, submitmsg):
"""sets a submitmsg if exists, using _cwmsgid mechanism """
cwmsgid = self._cw.set_redirect_message(submitmsg)
@@ -145,6 +159,16 @@
return getattr(self, '_form_previous_values', {})
return self.parent_form.form_previous_values
+ @property
+ def posting(self):
+ """return True if the form is being posted, False if it is being
+ generated.
+ """
+ # XXX check behaviour on regeneration after error
+ if self.parent_form is None:
+ return self._posting
+ return self.parent_form.posting
+
@iclassmethod
def _fieldsattr(cls_or_self):
if isinstance(cls_or_self, type):
--- a/web/formfields.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/formfields.py Tue Oct 23 15:00:53 2012 +0200
@@ -313,6 +313,7 @@
def role_name(self):
"""return <field.name>-<field.role> if role is specified, else field.name"""
+ assert self.name, 'field without a name (give it to constructor for explicitly built fields)'
if self.role is not None:
return role_name(self.name, self.role)
return self.name
@@ -360,7 +361,7 @@
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)
- return ()
+ return form.linked_to.get((self.name, self.role), ())
return None
def example_format(self, req):
--- a/web/formwidgets.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/formwidgets.py Tue Oct 23 15:00:53 2012 +0200
@@ -672,10 +672,11 @@
"""
needs_js = ('jquery.ui.js', )
needs_css = ('jquery.ui.css',)
+ default_size = 10
def __init__(self, datestr=None, **kwargs):
super(JQueryDatePicker, self).__init__(**kwargs)
- self.datestr = datestr
+ self.value = datestr
def _render(self, form, field, renderer):
req = form._cw
@@ -689,44 +690,36 @@
'{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'
' showOn: "button", buttonImageOnly: true})' % (
domid, req.uiprops['CALENDAR_ICON'], fmt))
- if self.datestr is None:
+ return self._render_input(form, field, domid)
+
+ def _render_input(self, form, field, domid):
+ if self.value is None:
value = self.values(form, field)[0]
else:
- value = self.datestr
- attrs = {}
- if self.settabindex:
- attrs['tabindex'] = req.next_tabindex()
- return tags.input(id=domid, name=domid, value=value,
- type='text', size='10', **attrs)
+ value = self.value
+ attrs = self.attributes(form, field)
+ attrs.setdefault('size', unicode(self.default_size))
+ return tags.input(name=domid, value=value, type='text', **attrs)
-class JQueryTimePicker(FieldWidget):
+class JQueryTimePicker(JQueryDatePicker):
"""Use jquery.timePicker to define a time picker. Will return the time as an
unicode string.
"""
needs_js = ('jquery.timePicker.js',)
needs_css = ('jquery.timepicker.css',)
+ default_size = 5
def __init__(self, timestr=None, timesteps=30, separator=u':', **kwargs):
- super(JQueryTimePicker, self).__init__(**kwargs)
- self.timestr = timestr
+ super(JQueryTimePicker, self).__init__(timestr, **kwargs)
self.timesteps = timesteps
self.separator = separator
def _render(self, form, field, renderer):
- req = form._cw
domid = field.dom_id(form, self.suffix)
- req.add_onload(u'cw.jqNode("%s").timePicker({selectedTime: "%s", step: %s, separator: "%s"})' % (
- domid, self.timestr, self.timesteps, self.separator))
- if self.timestr is None:
- value = self.values(form, field)[0]
- else:
- value = self.timestr
- attrs = {}
- if self.settabindex:
- attrs['tabindex'] = req.next_tabindex()
- return tags.input(id=domid, name=domid, value=value,
- type='text', size='5')
+ form._cw.add_onload(u'cw.jqNode("%s").timePicker({step: %s, separator: "%s"})' % (
+ domid, self.timesteps, self.separator))
+ return self._render_input(form, field, domid)
class JQueryDateTimePicker(FieldWidget):
--- a/web/http_headers.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/http_headers.py Tue Oct 23 15:00:53 2012 +0200
@@ -1290,11 +1290,13 @@
self._raw_headers[name] = r
return r
- def hasHeader(self, name):
+ def __contains__(self, name):
"""Does a header with the given name exist?"""
name=name.lower()
return self._raw_headers.has_key(name)
+ hasHeader = __contains__
+
def getRawHeaders(self, name, default=None):
"""Returns a list of headers matching the given name as the raw string given."""
--- a/web/httpcache.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/httpcache.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -147,3 +147,39 @@
viewmod.StartupView.http_cache_manager = MaxAgeHTTPCacheManager
viewmod.StartupView.cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
+
+
+### HTTP Cache validator ############################################
+
+
+
+def get_validators(headers_in):
+ """return a list of http condition validator relevant to this request
+ """
+ result = []
+ for header, func in VALIDATORS:
+ value = headers_in.getHeader(header)
+ if value is not None:
+ result.append((func, value))
+ return result
+
+
+def if_modified_since(ref_date, headers_out):
+ last_modified = headers_out.getHeader('last-modified')
+ if last_modified is None:
+ return True
+ return ref_date < last_modified
+
+def if_none_match(tags, headers_out):
+ etag = headers_out.getHeader('etag')
+ if etag is None:
+ return True
+ return not ((etag in tags) or ('*' in tags))
+
+VALIDATORS = [
+ ('if-modified-since', if_modified_since),
+ #('if-unmodified-since', if_unmodified_since),
+ ('if-none-match', if_none_match),
+ #('if-modified-since', if_modified_since),
+]
+
--- a/web/request.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/request.py Tue Oct 23 15:00:53 2012 +0200
@@ -25,8 +25,9 @@
from hashlib import sha1 # pylint: disable=E0611
from Cookie import SimpleCookie
from calendar import timegm
-from datetime import date
+from datetime import date, datetime
from urlparse import urlsplit
+import httplib
from itertools import count
from warnings import warn
@@ -43,8 +44,8 @@
from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
RequestError, StatusResponse)
-from cubicweb.web.httpcache import GMTOFFSET
-from cubicweb.web.http_headers import Headers, Cookie
+from cubicweb.web.httpcache import GMTOFFSET, get_validators
+from cubicweb.web.http_headers import Headers, Cookie, parseDateTime
_MARKER = object()
@@ -81,34 +82,53 @@
class CubicWebRequestBase(DBAPIRequest):
- """abstract HTTP request, should be extended according to the HTTP backend"""
+ """abstract HTTP request, should be extended according to the HTTP backend
+ Immutable attributes that describe the received query and generic configuration
+ """
ajax_request = False # to be set to True by ajax controllers
- def __init__(self, vreg, https, form=None):
+ def __init__(self, vreg, https=False, form=None, headers={}):
+ """
+ :vreg: Vregistry,
+ :https: boolean, s this a https request
+ :form: Forms value
+ """
super(CubicWebRequestBase, self).__init__(vreg)
+ #: (Boolean) Is this an https request.
self.https = https
+ #: User interface property (vary with https) (see :ref:`uiprops`)
+ self.uiprops = None
+ #: url for serving datadir (vary with https) (see :ref:`resources`)
+ self.datadir_url = None
if https:
self.uiprops = vreg.config.https_uiprops
self.datadir_url = vreg.config.https_datadir_url
else:
self.uiprops = vreg.config.uiprops
self.datadir_url = vreg.config.datadir_url
- # raw html headers that can be added from any view
+ #: raw html headers that can be added from any view
self.html_headers = HTMLHead(self)
- # form parameters
+ #: received headers
+ self._headers_in = Headers()
+ for k, v in headers.iteritems():
+ self._headers_in.addRawHeader(k, v)
+ #: form parameters
self.setup_params(form)
- # dictionary that may be used to store request data that has to be
- # shared among various components used to publish the request (views,
- # controller, application...)
+ #: dictionary that may be used to store request data that has to be
+ #: shared among various components used to publish the request (views,
+ #: controller, application...)
self.data = {}
- # search state: 'normal' or 'linksearch' (eg searching for an object
- # to create a relation with another)
+ #: search state: 'normal' or 'linksearch' (eg searching for an object
+ #: to create a relation with another)
self.search_state = ('normal',)
- # page id, set by htmlheader template
+ #: page id, set by htmlheader template
self.pageid = None
self._set_pageid()
# prepare output header
+ #: Header used for the final response
self.headers_out = Headers()
+ #: HTTP status use by the final response
+ self.status_out = 200
def _set_pageid(self):
"""initialize self.pageid
@@ -131,10 +151,30 @@
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
+ (see :ref:`WebServerConfig`)"""
return self.vreg.config['auth-mode']
+ # Various variable generator.
+
@property
def varmaker(self):
"""the rql varmaker is exposed both as a property and as the
@@ -186,14 +226,6 @@
# 3. default language
self.set_default_language(vreg)
- def set_language(self, lang):
- gettext, self.pgettext = self.translations[lang]
- self._ = self.__ = gettext
- self.lang = lang
- self.debug('request language: %s', lang)
- if self.cnx:
- self.cnx.set_session_props(lang=lang)
-
# input form parameters management ########################################
# common form parameters which should be protected against html values
@@ -269,7 +301,6 @@
form = self.form
return list_form_param(form, param, pop)
-
def reset_headers(self):
"""used by AutomaticWebTest to clear html headers between tests on
the same resultset
@@ -326,7 +357,7 @@
def update_search_state(self):
"""update the current search state"""
searchstate = self.form.get('__mode')
- if not searchstate and self.cnx:
+ if not searchstate:
searchstate = self.session.data.get('search_state', 'normal')
self.set_search_state(searchstate)
@@ -337,8 +368,7 @@
else:
self.search_state = ('linksearch', searchstate.split(':'))
assert len(self.search_state[-1]) == 4
- if self.cnx:
- self.session.data['search_state'] = searchstate
+ self.session.data['search_state'] = searchstate
def match_search_state(self, rset):
"""when searching an entity to create a relation, return True if entities in
@@ -711,14 +741,33 @@
return 'view'
def validate_cache(self):
- """raise a `DirectResponse` exception if a cached page along the way
+ """raise a `StatusResponse` exception if a cached page along the way
exists and is still usable.
calls the client-dependant implementation of `_validate_cache`
"""
- self._validate_cache()
- if self.http_method() == 'HEAD':
- raise StatusResponse(200, '')
+ modified = True
+ if self.get_header('Cache-Control') not in ('max-age=0', 'no-cache'):
+ # Here, we search for any invalid 'not modified' condition
+ # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3
+ validators = get_validators(self._headers_in)
+ if validators: # if we have no
+ modified = any(func(val, self.headers_out) for func, val in validators)
+ # Forge expected response
+ if modified:
+ if 'Expires' not in self.headers_out:
+ # Expires header seems to be required by IE7 -- Are you sure ?
+ self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
+ if self.http_method() == 'HEAD':
+ raise StatusResponse(200, '')
+ # /!\ no raise, the function returns and we keep processing the request)
+ else:
+ # overwrite headers_out to forge a brand new not-modified response
+ self.headers_out = self._forge_cached_headers()
+ if self.http_method() in ('HEAD', 'GET'):
+ raise StatusResponse(httplib.NOT_MODIFIED)
+ else:
+ raise StatusResponse(httplib.PRECONDITION_FAILED)
# abstract methods to override according to the web front-end #############
@@ -726,11 +775,19 @@
"""returns 'POST', 'GET', 'HEAD', etc."""
raise NotImplementedError()
- def _validate_cache(self):
- """raise a `DirectResponse` exception if a cached page along the way
- exists and is still usable
- """
- raise NotImplementedError()
+ def _forge_cached_headers(self):
+ # overwrite headers_out to forge a brand new not-modified response
+ headers = Headers()
+ for header in (
+ # Required from sec 10.3.5:
+ 'date', 'etag', 'content-location', 'expires',
+ 'cache-control', 'vary',
+ # Others:
+ 'server', 'proxy-authenticate', 'www-authenticate', 'warning'):
+ value = self._headers_in.getRawHeaders(header)
+ if value is not None:
+ headers.setRawHeaders(header, value)
+ return headers
def relative_path(self, includeparams=True):
"""return the normalized path of the request (ie at least relative
@@ -742,12 +799,37 @@
"""
raise NotImplementedError()
- def get_header(self, header, default=None):
- """return the value associated with the given input HTTP header,
- raise KeyError if the header is not set
+ # http headers ############################################################
+
+ ### incoming headers
+
+ def get_header(self, header, default=None, raw=True):
+ """return the value associated with the given input header, raise
+ KeyError if the header is not set
"""
- raise NotImplementedError()
+ if raw:
+ return self._headers_in.getRawHeaders(header, [default])[0]
+ return self._headers_in.getHeader(header, default)
+
+ def header_accept_language(self):
+ """returns an ordered list of preferred languages"""
+ acceptedlangs = self.get_header('Accept-Language', raw=False) or {}
+ for lang, _ in sorted(acceptedlangs.iteritems(), key=lambda x: x[1],
+ reverse=True):
+ lang = lang.split('-')[0]
+ yield lang
+ def header_if_modified_since(self):
+ """If the HTTP header If-modified-since is set, return the equivalent
+ date time value (GMT), else return None
+ """
+ mtime = self.get_header('If-modified-since', raw=False)
+ if mtime:
+ # :/ twisted is returned a localized time stamp
+ return datetime.fromtimestamp(mtime) + GMTOFFSET
+ return None
+
+ ### outcoming headers
def set_header(self, header, value, raw=True):
"""set an output HTTP header"""
if raw:
@@ -795,12 +877,6 @@
values = _parse_accept_header(accepteds, value_parser, value_sort_key)
return (raw_value for (raw_value, parsed_value, score) in values)
- def header_if_modified_since(self):
- """If the HTTP header If-modified-since is set, return the equivalent
- mx date time value (GMT), else return None
- """
- raise NotImplementedError()
-
def demote_to_html(self):
"""helper method to dynamically set request content type to text/html
@@ -815,6 +891,8 @@
self.set_content_type('text/html')
self.main_stream.set_doctype(TRANSITIONAL_DOCTYPE_NOEXT)
+ # xml doctype #############################################################
+
def set_doctype(self, doctype, reset_xmldecl=True):
"""helper method to dynamically change page doctype
--- a/web/test/data/views.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/data/views.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,18 +15,16 @@
#
# 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 import Redirect
from cubicweb.web.application import CubicWebPublisher
-# proof of concept : monkey patch publish method so that if we are in an
+# proof of concept : monkey patch handle method so that if we are in an
# anonymous session and __fblogin is found is req.form, the user with the
# given login is created if necessary and then a session is opened for that
# user
# NOTE: this require "cookie" authentication mode
-def auto_login_publish(self, path, req):
+def auto_login_handle_request(self, req, path):
if (not req.cnx or req.cnx.anonymous_connection) and req.form.get('__fblogin'):
login = password = req.form.pop('__fblogin')
self.repo.register_user(login, password)
@@ -40,7 +38,7 @@
except Redirect:
pass
assert req.user.login == login
- return orig_publish(self, path, req)
+ return orig_handle(self, req, path)
-orig_publish = CubicWebPublisher.main_publish
-CubicWebPublisher.main_publish = auto_login_publish
+orig_handle = CubicWebPublisher.main_handle_request
+CubicWebPublisher.main_handle_request = auto_login_handle_request
--- a/web/test/unittest_application.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_application.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -184,12 +184,12 @@
def test_nonregr_publish1(self):
req = self.request(u'CWEType X WHERE X final FALSE, X meta FALSE')
- self.app.publish('view', req)
+ self.app.handle_request(req, 'view')
def test_nonregr_publish2(self):
req = self.request(u'Any count(N) WHERE N todo_by U, N is Note, U eid %s'
% self.user().eid)
- self.app.publish('view', req)
+ self.app.handle_request(req, 'view')
def test_publish_validation_error(self):
req = self.request()
@@ -202,7 +202,7 @@
# just a sample, missing some necessary information for real life
'__errorurl': 'view?vid=edition...'
}
- path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+ path, params = self.expect_redirect_handle_request(req, 'edit')
forminfo = req.session.data['view?vid=edition...']
eidmap = forminfo['eidmap']
self.assertEqual(eidmap, {})
@@ -232,7 +232,7 @@
# necessary to get validation error handling
'__errorurl': 'view?vid=edition...',
}
- path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+ path, params = self.expect_redirect_handle_request(req, 'edit')
forminfo = req.session.data['view?vid=edition...']
self.assertEqual(set(forminfo['eidmap']), set('XY'))
self.assertEqual(forminfo['eidmap']['X'], None)
@@ -261,7 +261,7 @@
# necessary to get validation error handling
'__errorurl': 'view?vid=edition...',
}
- path, params = self.expect_redirect(lambda x: self.app_publish(x, 'edit'), req)
+ path, params = self.expect_redirect_handle_request(req, 'edit')
forminfo = req.session.data['view?vid=edition...']
self.assertEqual(set(forminfo['eidmap']), set('XY'))
self.assertIsInstance(forminfo['eidmap']['X'], int)
@@ -274,7 +274,7 @@
def _test_cleaned(self, kwargs, injected, cleaned):
req = self.request(**kwargs)
- page = self.app.publish('view', req)
+ page = self.app.handle_request(req, 'view')
self.assertFalse(injected in page, (kwargs, injected))
self.assertTrue(cleaned in page, (kwargs, cleaned))
@@ -308,12 +308,6 @@
self.commit()
self.assertEqual(vreg.property_value('ui.language'), 'en')
- def test_login_not_available_to_authenticated(self):
- req = self.request()
- with self.assertRaises(Unauthorized) as cm:
- self.app_publish(req, 'login')
- self.assertEqual(str(cm.exception), 'log out first')
-
def test_fb_login_concept(self):
"""see data/views.py"""
self.set_auth_mode('cookie', 'anon')
@@ -321,7 +315,7 @@
req = self.request()
origcnx = req.cnx
req.form['__fblogin'] = u'turlututu'
- page = self.app_publish(req)
+ page = self.app.handle_request(req, '')
self.assertFalse(req.cnx is origcnx)
self.assertEqual(req.user.login, 'turlututu')
self.assertTrue('turlututu' in page, page)
@@ -332,25 +326,28 @@
def test_http_auth_no_anon(self):
req, origsession = self.init_authentication('http')
self.assertAuthFailure(req)
- self.assertRaises(AuthenticationError, self.app_publish, req, 'login')
+ self.assertRaises(AuthenticationError, self.app_handle_request, req, 'login')
self.assertEqual(req.cnx, None)
authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword))
req.set_request_header('Authorization', 'basic %s' % authstr)
self.assertAuthSuccess(req, origsession)
- self.assertRaises(LogOut, self.app_publish, req, 'logout')
+ self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
self.assertEqual(len(self.open_sessions), 0)
def test_cookie_auth_no_anon(self):
req, origsession = self.init_authentication('cookie')
self.assertAuthFailure(req)
- form = self.app_publish(req, 'login')
+ try:
+ form = self.app_handle_request(req, 'login')
+ except Redirect, redir:
+ self.fail('anonymous user should get login form')
self.assertTrue('__login' in form)
self.assertTrue('__password' in form)
self.assertEqual(req.cnx, None)
req.form['__login'] = self.admlogin
req.form['__password'] = self.admpassword
self.assertAuthSuccess(req, origsession)
- self.assertRaises(LogOut, self.app_publish, req, 'logout')
+ self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
self.assertEqual(len(self.open_sessions), 0)
def test_login_by_email(self):
@@ -370,7 +367,7 @@
req.form['__login'] = address
req.form['__password'] = self.admpassword
self.assertAuthSuccess(req, origsession)
- self.assertRaises(LogOut, self.app_publish, req, 'logout')
+ self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
self.assertEqual(len(self.open_sessions), 0)
def _reset_cookie(self, req):
@@ -410,7 +407,7 @@
authstr = base64.encodestring('%s:%s' % (self.admlogin, self.admpassword))
req.set_request_header('Authorization', 'basic %s' % authstr)
self.assertAuthSuccess(req, origsession)
- self.assertRaises(LogOut, self.app_publish, req, 'logout')
+ self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
self.assertEqual(len(self.open_sessions), 0)
def test_cookie_auth_anon_allowed(self):
@@ -422,7 +419,7 @@
req.form['__login'] = self.admlogin
req.form['__password'] = self.admpassword
self.assertAuthSuccess(req, origsession)
- self.assertRaises(LogOut, self.app_publish, req, 'logout')
+ self.assertRaises(LogOut, self.app_handle_request, req, 'logout')
self.assertEqual(len(self.open_sessions), 0)
def test_anonymized_request(self):
@@ -441,7 +438,7 @@
req = self.request()
# expect a rset with None in [0][0]
req.form['rql'] = 'rql:Any OV1, X WHERE X custom_workflow OV1?'
- self.app_publish(req)
+ self.app_handle_request(req)
if __name__ == '__main__':
unittest_main()
--- a/web/test/unittest_formfields.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_formfields.py Tue Oct 23 15:00:53 2012 +0200
@@ -147,7 +147,7 @@
def test_property_key_field(self):
from cubicweb.web.views.cwproperties import PropertyKeyField
req = self.request()
- field = PropertyKeyField()
+ field = PropertyKeyField(name='test')
e = self.vreg['etypes'].etype_class('CWProperty')(req)
renderer = self.vreg['formrenderers'].select('base', req)
form = EntityFieldsForm(req, entity=e)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_http.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,282 @@
+from logilab.common.testlib import TestCase, unittest_main, tag, Tags
+
+from cubicweb.web import StatusResponse
+from cubicweb.devtools.fake import FakeRequest
+
+
+def _test_cache(hin, hout, method='GET'):
+ """forge and process a request
+
+ return status code and the request object
+
+ status is None is no cache is involved
+ """
+ # forge request
+ req = FakeRequest(method=method)
+ for key, value in hin:
+ req._headers_in.addRawHeader(key, str(value))
+ for key, value in hout:
+ req.headers_out.addRawHeader(key, str(value))
+ # process
+ status = None
+ try:
+ req.validate_cache()
+ except StatusResponse, ex:
+ status = ex.status
+ return status, req
+
+class HTTPCache(TestCase):
+ """Check that the http cache logiac work as expected
+ (as far as we understood the RFC)
+
+ """
+ tags = TestCase.tags | Tags('http', 'cache')
+
+
+ def assertCache(self, expected, status, situation=''):
+ """simple assert for nicer message"""
+ if expected != status:
+ if expected is None:
+ expected = "MODIFIED"
+ if status is None:
+ status = "MODIFIED"
+ msg = 'expected %r got %r' % (expected, status)
+ if situation:
+ msg = "%s - when: %s" % (msg, situation)
+ self.fail(msg)
+
+ def test_IN_none_OUT_none(self):
+ #: test that no caching is requested when not data is available
+ #: on any side
+ status, req =_test_cache((),())
+ self.assertIsNone(status)
+
+ def test_IN_Some_OUT_none(self):
+ #: test that no caching is requested when no data is available
+ #: server (origin) side
+ hin = [('if-modified-since','Sat, 14 Apr 2012 14:39:32 GM'),
+ ]
+ status, req = _test_cache(hin, ())
+ self.assertIsNone(status)
+ hin = [('if-none-match','babar/huitre'),
+ ]
+ status, req = _test_cache(hin, ())
+ self.assertIsNone(status)
+ hin = [('if-modified-since','Sat, 14 Apr 2012 14:39:32 GM'),
+ ('if-none-match','babar/huitre'),
+ ]
+ status, req = _test_cache(hin, ())
+ self.assertIsNone(status)
+
+ def test_IN_none_OUT_Some(self):
+ #: test that no caching is requested when no data is provided
+ #: by the client
+ hout = [('last-modified','Sat, 14 Apr 2012 14:39:32 GM'),
+ ]
+ status, req = _test_cache((), hout)
+ self.assertIsNone(status)
+ hout = [('etag','babar/huitre'),
+ ]
+ status, req = _test_cache((), hout)
+ self.assertIsNone(status)
+ hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+ ('etag','babar/huitre'),
+ ]
+ status, req = _test_cache((), hout)
+ self.assertIsNone(status)
+
+ @tag('last_modified')
+ def test_last_modified_newer(self):
+ #: test the proper behavior of modification date only
+ # newer
+ hin = [('if-modified-since', 'Sat, 13 Apr 2012 14:39:32 GM'),
+ ]
+ hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(None, status, 'origin is newer than client')
+
+ @tag('last_modified')
+ def test_last_modified_older(self):
+ # older
+ hin = [('if-modified-since', 'Sat, 15 Apr 2012 14:39:32 GM'),
+ ]
+ hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(304, status, 'origin is older than client')
+
+ @tag('last_modified')
+ def test_last_modified_same(self):
+ # same
+ hin = [('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+ ]
+ hout = [('last-modified', 'Sat, 14 Apr 2012 14:39:32 GM'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(304, status, 'origin is equal to client')
+
+ @tag('etag')
+ def test_etag_mismatch(self):
+ #: test the proper behavior of etag only
+ # etag mismatch
+ hin = [('if-none-match', 'babar'),
+ ]
+ hout = [('etag', 'celestine'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(None, status, 'etag mismatch')
+
+ @tag('etag')
+ def test_etag_match(self):
+ # etag match
+ hin = [('if-none-match', 'babar'),
+ ]
+ hout = [('etag', 'babar'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(304, status, 'etag match')
+ # etag match in multiple
+ hin = [('if-none-match', 'loutre'),
+ ('if-none-match', 'babar'),
+ ]
+ hout = [('etag', 'babar'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(304, status, 'etag match in multiple')
+ # client use "*" as etag
+ hin = [('if-none-match', '*'),
+ ]
+ hout = [('etag', 'babar'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(304, status, 'client use "*" as etag')
+
+ @tag('etag', 'last_modified')
+ def test_both(self):
+ #: test the proper behavior of etag only
+ # both wrong
+ hin = [('if-none-match', 'babar'),
+ ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+ ]
+ hout = [('etag', 'loutre'),
+ ('last-modified', 'Sat, 15 Apr 2012 14:39:32 GM'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(None, status, 'both wrong')
+
+ @tag('etag', 'last_modified')
+ def test_both_etag_mismatch(self):
+ # both etag mismatch
+ hin = [('if-none-match', 'babar'),
+ ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+ ]
+ hout = [('etag', 'loutre'),
+ ('last-modified', 'Sat, 13 Apr 2012 14:39:32 GM'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(None, status, 'both but etag mismatch')
+
+ @tag('etag', 'last_modified')
+ def test_both_but_modified(self):
+ # both but modified
+ hin = [('if-none-match', 'babar'),
+ ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+ ]
+ hout = [('etag', 'babar'),
+ ('last-modified', 'Sat, 15 Apr 2012 14:39:32 GM'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(None, status, 'both but modified')
+
+ @tag('etag', 'last_modified')
+ def test_both_ok(self):
+ # both ok
+ hin = [('if-none-match', 'babar'),
+ ('if-modified-since', 'Sat, 14 Apr 2012 14:39:32 GM'),
+ ]
+ hout = [('etag', 'babar'),
+ ('last-modified', 'Sat, 13 Apr 2012 14:39:32 GM'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(304, status, 'both ok')
+
+ @tag('etag', 'HEAD')
+ def test_head_verb(self):
+ #: check than FOUND 200 is properly raise without content on HEAD request
+ #: This logic does not really belong here :-/
+ # modified
+ hin = [('if-none-match', 'babar'),
+ ]
+ hout = [('etag', 'rhino/really-not-babar'),
+ ]
+ status, req = _test_cache(hin, hout, method='HEAD')
+ self.assertCache(200, status, 'modifier HEAD verb')
+ # not modified
+ hin = [('if-none-match', 'babar'),
+ ]
+ hout = [('etag', 'babar'),
+ ]
+ status, req = _test_cache(hin, hout, method='HEAD')
+ self.assertCache(304, status, 'not modifier HEAD verb')
+
+ @tag('etag', 'POST')
+ def test_post_verb(self):
+ # modified
+ hin = [('if-none-match', 'babar'),
+ ]
+ hout = [('etag', 'rhino/really-not-babar'),
+ ]
+ status, req = _test_cache(hin, hout, method='POST')
+ self.assertCache(None, status, 'modifier HEAD verb')
+ # not modified
+ hin = [('if-none-match', 'babar'),
+ ]
+ hout = [('etag', 'babar'),
+ ]
+ status, req = _test_cache(hin, hout, method='POST')
+ self.assertCache(412, status, 'not modifier HEAD verb')
+
+ @tag('expires')
+ def test_expires_added(self):
+ #: Check that Expires header is added:
+ #: - when the page is modified
+ #: - when none was already present
+ hin = [('if-none-match', 'babar'),
+ ]
+ hout = [('etag', 'rhino/really-not-babar'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(None, status, 'modifier HEAD verb')
+ value = req.headers_out.getHeader('expires')
+ self.assertIsNotNone(value)
+
+ @tag('expires')
+ def test_expires_not_added(self):
+ #: Check that Expires header is not added if NOT-MODIFIED
+ hin = [('if-none-match', 'babar'),
+ ]
+ hout = [('etag', 'babar'),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(304, status, 'not modifier HEAD verb')
+ value = req.headers_out.getHeader('expires')
+ self.assertIsNone(value)
+
+ @tag('expires')
+ def test_expires_no_overwrite(self):
+ #: Check that cache does not overwrite existing Expires header
+ hin = [('if-none-match', 'babar'),
+ ]
+ DATE = 'Sat, 13 Apr 2012 14:39:32 GM'
+ hout = [('etag', 'rhino/really-not-babar'),
+ ('expires', DATE),
+ ]
+ status, req = _test_cache(hin, hout)
+ self.assertCache(None, status, 'not modifier HEAD verb')
+ value = req.headers_out.getRawHeaders('expires')
+ self.assertEqual(value, [DATE])
+
+
+if __name__ == '__main__':
+ unittest_main()
--- a/web/test/unittest_magicsearch.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_magicsearch.py Tue Oct 23 15:00:53 2012 +0200
@@ -230,5 +230,118 @@
self.assertEqual(rset.rql, 'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s')
self.assertEqual(rset.args, {'text': u'utilisateur Smith'})
+
+class RQLSuggestionsBuilderTC(CubicWebTC):
+ def suggestions(self, rql):
+ req = self.request()
+ rbs = self.vreg['components'].select('rql.suggestions', req)
+ return rbs.build_suggestions(rql)
+
+ def test_no_restrictions_rql(self):
+ self.assertListEqual([], self.suggestions(''))
+ self.assertListEqual([], self.suggestions('An'))
+ self.assertListEqual([], self.suggestions('Any X'))
+ self.assertListEqual([], self.suggestions('Any X, Y'))
+
+ def test_invalid_rql(self):
+ self.assertListEqual([], self.suggestions('blabla'))
+ self.assertListEqual([], self.suggestions('Any X WHERE foo, bar'))
+
+ def test_is_rql(self):
+ self.assertListEqual(['Any X WHERE X is %s' % eschema
+ for eschema in sorted(self.vreg.schema.entities())
+ if not eschema.final],
+ self.suggestions('Any X WHERE X is'))
+
+ self.assertListEqual(['Any X WHERE X is Personne', 'Any X WHERE X is Project'],
+ self.suggestions('Any X WHERE X is P'))
+
+ self.assertListEqual(['Any X WHERE X is Personne, Y is Personne',
+ 'Any X WHERE X is Personne, Y is Project'],
+ self.suggestions('Any X WHERE X is Personne, Y is P'))
+
+
+ def test_relations_rql(self):
+ self.assertListEqual(['Any X WHERE X is Personne, X ass A',
+ 'Any X WHERE X is Personne, X datenaiss A',
+ 'Any X WHERE X is Personne, X description A',
+ 'Any X WHERE X is Personne, X fax A',
+ 'Any X WHERE X is Personne, X nom A',
+ 'Any X WHERE X is Personne, X prenom A',
+ 'Any X WHERE X is Personne, X promo A',
+ 'Any X WHERE X is Personne, X salary A',
+ 'Any X WHERE X is Personne, X sexe A',
+ 'Any X WHERE X is Personne, X tel A',
+ 'Any X WHERE X is Personne, X test A',
+ 'Any X WHERE X is Personne, X titre A',
+ 'Any X WHERE X is Personne, X travaille A',
+ 'Any X WHERE X is Personne, X web A',
+ ],
+ self.suggestions('Any X WHERE X is Personne, X '))
+ self.assertListEqual(['Any X WHERE X is Personne, X tel A',
+ 'Any X WHERE X is Personne, X test A',
+ 'Any X WHERE X is Personne, X titre A',
+ 'Any X WHERE X is Personne, X travaille A',
+ ],
+ self.suggestions('Any X WHERE X is Personne, X t'))
+ # try completion on selected
+ self.assertListEqual(['Any X WHERE X is Personne, Y is Societe, X tel A',
+ 'Any X WHERE X is Personne, Y is Societe, X test A',
+ 'Any X WHERE X is Personne, Y is Societe, X titre A',
+ 'Any X WHERE X is Personne, Y is Societe, X travaille Y',
+ ],
+ self.suggestions('Any X WHERE X is Personne, Y is Societe, X t'))
+ # invalid relation should not break
+ self.assertListEqual([],
+ self.suggestions('Any X WHERE X is Personne, X asdasd'))
+
+ def test_attribute_vocabulary_rql(self):
+ self.assertListEqual(['Any X WHERE X is Personne, X promo "bon"',
+ 'Any X WHERE X is Personne, X promo "pasbon"',
+ ],
+ self.suggestions('Any X WHERE X is Personne, X promo "'))
+ self.assertListEqual(['Any X WHERE X is Personne, X promo "pasbon"',
+ ],
+ self.suggestions('Any X WHERE X is Personne, X promo "p'))
+ # "bon" should be considered complete, hence no suggestion
+ self.assertListEqual([],
+ self.suggestions('Any X WHERE X is Personne, X promo "bon"'))
+ # no valid vocabulary starts with "po"
+ self.assertListEqual([],
+ self.suggestions('Any X WHERE X is Personne, X promo "po'))
+
+ def test_attribute_value_rql(self):
+ # suggestions should contain any possible value for
+ # a given attribute (limited to 10)
+ req = self.request()
+ for i in xrange(15):
+ req.create_entity('Personne', nom=u'n%s' % i, prenom=u'p%s' % i)
+ self.assertListEqual(['Any X WHERE X is Personne, X nom "n0"',
+ 'Any X WHERE X is Personne, X nom "n1"',
+ 'Any X WHERE X is Personne, X nom "n10"',
+ 'Any X WHERE X is Personne, X nom "n11"',
+ 'Any X WHERE X is Personne, X nom "n12"',
+ 'Any X WHERE X is Personne, X nom "n13"',
+ 'Any X WHERE X is Personne, X nom "n14"',
+ 'Any X WHERE X is Personne, X nom "n2"',
+ 'Any X WHERE X is Personne, X nom "n3"',
+ 'Any X WHERE X is Personne, X nom "n4"',
+ 'Any X WHERE X is Personne, X nom "n5"',
+ 'Any X WHERE X is Personne, X nom "n6"',
+ 'Any X WHERE X is Personne, X nom "n7"',
+ 'Any X WHERE X is Personne, X nom "n8"',
+ 'Any X WHERE X is Personne, X nom "n9"',
+ ],
+ self.suggestions('Any X WHERE X is Personne, X nom "'))
+ self.assertListEqual(['Any X WHERE X is Personne, X nom "n1"',
+ 'Any X WHERE X is Personne, X nom "n10"',
+ 'Any X WHERE X is Personne, X nom "n11"',
+ 'Any X WHERE X is Personne, X nom "n12"',
+ 'Any X WHERE X is Personne, X nom "n13"',
+ 'Any X WHERE X is Personne, X nom "n14"',
+ ],
+ self.suggestions('Any X WHERE X is Personne, X nom "n1'))
+
+
if __name__ == '__main__':
unittest_main()
--- a/web/test/unittest_reledit.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_reledit.py Tue Oct 23 15:00:53 2012 +0200
@@ -175,8 +175,8 @@
def setup_database(self):
super(ClickAndEditFormUICFGTC, self).setup_database()
- self.tick.set_relations(concerns=self.proj)
- self.proj.set_relations(manager=self.toto)
+ self.tick.cw_set(concerns=self.proj)
+ self.proj.cw_set(manager=self.toto)
def test_with_uicfg(self):
old_rctl = reledit_ctrl._tagdefs.copy()
--- a/web/test/unittest_request.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_request.py Tue Oct 23 15:00:53 2012 +0200
@@ -5,7 +5,9 @@
from functools import partial
-from cubicweb.web.request import (_parse_accept_header,
+from cubicweb.devtools.fake import FakeConfig
+
+from cubicweb.web.request import (CubicWebRequestBase, _parse_accept_header,
_mimetype_sort_key, _mimetype_parser, _charset_sort_key)
@@ -65,5 +67,23 @@
('utf-8', 'utf-8', 0.7),
('*', '*', 0.7)])
+ def test_base_url(self):
+ dummy_vreg = type('DummyVreg', (object,), {})()
+ dummy_vreg.config = FakeConfig()
+ 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))
+
+
+
if __name__ == '__main__':
unittest_main()
--- a/web/test/unittest_urlrewrite.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_urlrewrite.py Tue Oct 23 15:00:53 2012 +0200
@@ -105,9 +105,9 @@
def setup_database(self):
req = self.request()
self.p1 = self.create_user(req, u'user1')
- self.p1.set_attributes(firstname=u'joe', surname=u'Dalton')
+ self.p1.cw_set(firstname=u'joe', surname=u'Dalton')
self.p2 = self.create_user(req, u'user2')
- self.p2.set_attributes(firstname=u'jack', surname=u'Dalton')
+ self.p2.cw_set(firstname=u'jack', surname=u'Dalton')
def test_rgx_action_with_transforms(self):
class TestSchemaBasedRewriter(SchemaBasedRewriter):
--- a/web/test/unittest_views_basecontrollers.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_views_basecontrollers.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -19,6 +19,12 @@
from __future__ import with_statement
+from urlparse import urlsplit, urlunsplit, urljoin
+# parse_qs is deprecated in cgi and has been moved to urlparse in Python 2.6
+try:
+ from urlparse import parse_qs as url_parse_query
+except ImportError:
+ from cgi import parse_qs as url_parse_query
from logilab.common.testlib import unittest_main, mock_object
from logilab.common.decorators import monkeypatch
@@ -32,6 +38,7 @@
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
u = unicode
@@ -70,6 +77,7 @@
}
with self.assertRaises(ValidationError) as cm:
self.ctrl_publish(req)
+ cm.exception.tr(unicode)
self.assertEqual(cm.exception.errors, {'login-subject': 'the value "admin" is already used, use another one'})
def test_user_editing_itself(self):
@@ -89,7 +97,7 @@
'firstname-subject:'+eid: u'Sylvain',
'in_group-subject:'+eid: groups,
}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
self.assertEqual(e.firstname, u'Sylvain')
self.assertEqual(e.surname, u'Th\xe9nault')
@@ -108,7 +116,7 @@
'upassword-subject:'+eid: 'tournicoton',
'upassword-subject-confirm:'+eid: 'tournicoton',
}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
cnx.commit() # commit to check we don't get late validation error for instance
self.assertEqual(path, 'cwuser/user')
self.assertFalse('vid' in params)
@@ -129,7 +137,7 @@
'firstname-subject:'+eid: u'Th\xe9nault',
'surname-subject:'+eid: u'Sylvain',
}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
e = self.execute('Any X WHERE X eid %(x)s', {'x': user.eid}).get_entity(0, 0)
self.assertEqual(e.login, user.login)
self.assertEqual(e.firstname, u'Th\xe9nault')
@@ -155,7 +163,7 @@
'address-subject:Y': u'dima@logilab.fr',
'use_email-object:Y': 'X',
}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
# should be redirected on the created person
self.assertEqual(path, 'cwuser/adim')
e = self.execute('Any P WHERE P surname "Di Mascio"').get_entity(0, 0)
@@ -177,7 +185,7 @@
'address-subject:Y': u'dima@logilab.fr',
'use_email-object:Y': peid,
}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
# should be redirected on the created person
self.assertEqual(path, 'cwuser/adim')
e = self.execute('Any P WHERE P surname "Di Masci"').get_entity(0, 0)
@@ -197,7 +205,7 @@
'address-subject:'+emaileid: u'adim@logilab.fr',
'use_email-object:'+emaileid: peid,
}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
email.cw_clear_all_caches()
self.assertEqual(email.address, 'adim@logilab.fr')
@@ -242,6 +250,7 @@
}
with self.assertRaises(ValidationError) as cm:
self.ctrl_publish(req)
+ cm.exception.tr(unicode)
self.assertEqual(cm.exception.errors, {'amount-subject': 'value -10 must be >= 0'})
req = self.request(rollbackfirst=True)
req.form = {'eid': ['X'],
@@ -252,6 +261,7 @@
}
with self.assertRaises(ValidationError) as cm:
self.ctrl_publish(req)
+ cm.exception.tr(unicode)
self.assertEqual(cm.exception.errors, {'amount-subject': 'value 110 must be <= 100'})
req = self.request(rollbackfirst=True)
req.form = {'eid': ['X'],
@@ -260,7 +270,7 @@
'amount-subject:X': u'10',
'described_by_test-subject:X': u(feid),
}
- self.expect_redirect_publish(req, 'edit')
+ self.expect_redirect_handle_request(req, 'edit')
# should be redirected on the created
#eid = params['rql'].split()[-1]
e = self.execute('Salesterm X').get_entity(0, 0)
@@ -272,7 +282,7 @@
user = self.user()
req = self.request(**req_form(user))
req.session.data['pending_insert'] = set([(user.eid, 'in_group', tmpgroup.eid)])
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
usergroups = [gname for gname, in
self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
self.assertItemsEqual(usergroups, ['managers', 'test'])
@@ -291,7 +301,7 @@
# now try to delete the relation
req = self.request(**req_form(user))
req.session.data['pending_delete'] = set([(user.eid, 'in_group', groupeid)])
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
usergroups = [gname for gname, in
self.execute('Any N WHERE G name N, U in_group G, U eid %(u)s', {'u': user.eid})]
self.assertItemsEqual(usergroups, ['managers'])
@@ -311,7 +321,7 @@
'__form_id': 'edition',
'__action_apply': '',
}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
self.assertTrue(path.startswith('blogentry/'))
eid = path.split('/')[1]
self.assertEqual(params['vid'], 'edition')
@@ -333,7 +343,7 @@
'__redirectparams': 'toto=tutu&tata=titi',
'__form_id': 'edition',
}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
self.assertEqual(path, 'view')
self.assertEqual(params['rql'], redirectrql)
self.assertEqual(params['vid'], 'primary')
@@ -345,7 +355,7 @@
eid = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
req.form = {'eid': u(eid), '__type:%s'%eid: 'BlogEntry',
'__action_delete': ''}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
self.assertEqual(path, 'blogentry')
self.assertIn('_cwmsgid', params)
eid = req.create_entity('EmailAddress', address=u'hop@logilab.fr').eid
@@ -355,7 +365,7 @@
req = req
req.form = {'eid': u(eid), '__type:%s'%eid: 'EmailAddress',
'__action_delete': ''}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
self.assertEqual(path, 'cwuser/admin')
self.assertIn('_cwmsgid', params)
eid1 = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
@@ -365,7 +375,7 @@
'__type:%s'%eid1: 'BlogEntry',
'__type:%s'%eid2: 'EmailAddress',
'__action_delete': ''}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
self.assertEqual(path, 'view')
self.assertIn('_cwmsgid', params)
@@ -381,7 +391,7 @@
'title-subject:X': u'entry1-copy',
'content-subject:X': u'content1',
}
- self.expect_redirect_publish(req, 'edit')
+ self.expect_redirect_handle_request(req, 'edit')
blogentry2 = req.find_one_entity('BlogEntry', title=u'entry1-copy')
self.assertEqual(blogentry2.entry_of[0].eid, blog.eid)
@@ -399,7 +409,7 @@
'title-subject:X': u'entry1-copy',
'content-subject:X': u'content1',
}
- self.expect_redirect_publish(req, 'edit')
+ self.expect_redirect_handle_request(req, 'edit')
blogentry2 = req.find_one_entity('BlogEntry', title=u'entry1-copy')
# entry_of should not be copied
self.assertEqual(len(blogentry2.entry_of), 0)
@@ -425,7 +435,7 @@
'read_permission-subject:'+cwetypeeid: groups,
}
try:
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
e = self.execute('Any X WHERE X eid %(x)s', {'x': cwetypeeid}).get_entity(0, 0)
self.assertEqual(e.name, 'CWEType')
self.assertEqual(sorted(g.eid for g in e.read_permission), groupeids)
@@ -445,7 +455,7 @@
'__type:A': 'BlogEntry', '_cw_entity_fields:A': 'title-subject,content-subject',
'title-subject:A': u'"13:03:40"',
'content-subject:A': u'"13:03:43"',}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
self.assertTrue(path.startswith('blogentry/'))
eid = path.split('/')[1]
e = self.execute('Any C, T WHERE C eid %(x)s, C content T', {'x': eid}).get_entity(0, 0)
@@ -483,7 +493,7 @@
'login-subject:X': u'toto',
'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
}
- path, params = self.expect_redirect_publish(req, 'edit')
+ path, params = self.expect_redirect_handle_request(req, 'edit')
self.assertEqual(path, 'cwuser/toto')
e = self.execute('Any X WHERE X is CWUser, X login "toto"').get_entity(0, 0)
self.assertEqual(e.login, 'toto')
@@ -513,12 +523,12 @@
# which fires a Redirect
# 2/ When re-publishing the copy form, the publisher implicitly commits
try:
- self.app_publish(req, 'edit')
+ self.app_handle_request(req, 'edit')
except Redirect:
req = self.request()
req.form['rql'] = 'Any X WHERE X eid %s' % p.eid
req.form['vid'] = 'copy'
- self.app_publish(req, 'view')
+ self.app_handle_request(req, 'view')
rset = self.execute('CWUser P WHERE P surname "Boom"')
self.assertEqual(len(rset), 0)
finally:
@@ -688,38 +698,44 @@
@ajaxfunc
def foo(self, x, y):
return 'hello'
- self.assertTrue(issubclass(foo, AjaxFunction))
- self.assertEqual(foo.__regid__, 'foo')
- self.assertEqual(foo.check_pageid, False)
- self.assertEqual(foo.output_type, None)
+ self.assertEqual(foo(object, 1, 2), 'hello')
+ appobject = foo.__appobject__
+ self.assertTrue(issubclass(appobject, AjaxFunction))
+ self.assertEqual(appobject.__regid__, 'foo')
+ self.assertEqual(appobject.check_pageid, False)
+ self.assertEqual(appobject.output_type, None)
req = self.request()
- f = foo(req)
+ f = appobject(req)
self.assertEqual(f(12, 13), 'hello')
def test_ajaxfunc_checkpageid(self):
- @ajaxfunc( check_pageid=True)
+ @ajaxfunc(check_pageid=True)
def foo(self, x, y):
- pass
- self.assertTrue(issubclass(foo, AjaxFunction))
- self.assertEqual(foo.__regid__, 'foo')
- self.assertEqual(foo.check_pageid, True)
- self.assertEqual(foo.output_type, None)
+ return 'hello'
+ self.assertEqual(foo(object, 1, 2), 'hello')
+ appobject = foo.__appobject__
+ self.assertTrue(issubclass(appobject, AjaxFunction))
+ self.assertEqual(appobject.__regid__, 'foo')
+ self.assertEqual(appobject.check_pageid, True)
+ self.assertEqual(appobject.output_type, None)
# no pageid
req = self.request()
- f = foo(req)
+ f = appobject(req)
self.assertRaises(RemoteCallFailed, f, 12, 13)
def test_ajaxfunc_json(self):
@ajaxfunc(output_type='json')
def foo(self, x, y):
return x + y
- self.assertTrue(issubclass(foo, AjaxFunction))
- self.assertEqual(foo.__regid__, 'foo')
- self.assertEqual(foo.check_pageid, False)
- self.assertEqual(foo.output_type, 'json')
+ self.assertEqual(foo(object, 1, 2), 3)
+ appobject = foo.__appobject__
+ self.assertTrue(issubclass(appobject, AjaxFunction))
+ self.assertEqual(appobject.__regid__, 'foo')
+ self.assertEqual(appobject.check_pageid, False)
+ self.assertEqual(appobject.output_type, 'json')
# no pageid
req = self.request()
- f = foo(req)
+ f = appobject(req)
self.assertEqual(f(12, 13), '25')
@@ -768,5 +784,86 @@
res, req = self.remote_call('foo')
self.assertEqual(res, '12')
+ def test_monkeypatch_jsoncontroller_stdfunc(self):
+ @monkeypatch(JSonController)
+ @jsonize
+ def js_reledit_form(self):
+ return 12
+ res, req = self.remote_call('reledit_form')
+ self.assertEqual(res, '12')
+
+
+class UndoControllerTC(CubicWebTC):
+
+ def setup_database(self):
+ req = self.request()
+ self.session.undo_actions = True
+ self.toto = self.create_user(req, 'toto', password='toto', groups=('users',),
+ commit=False)
+ self.txuuid_toto = self.commit()
+ self.toto_email = self.session.create_entity('EmailAddress',
+ address=u'toto@logilab.org',
+ reverse_use_email=self.toto)
+ self.txuuid_toto_email = self.commit()
+
+ def test_no_such_transaction(self):
+ req = self.request()
+ txuuid = u"12345acbd"
+ req.form['txuuid'] = txuuid
+ controller = self.vreg['controllers'].select('undo', req)
+ with self.assertRaises(tx.NoSuchTransaction) as cm:
+ result = controller.publish(rset=None)
+ self.assertEqual(cm.exception.txuuid, txuuid)
+
+ def assertURLPath(self, url, expected_path, expected_params=None):
+ """ This assert that the path part of `url` matches expected path
+
+ TODO : implement assertion on the expected_params too
+ """
+ req = self.request()
+ scheme, netloc, path, query, fragment = urlsplit(url)
+ query_dict = url_parse_query(query)
+ expected_url = urljoin(req.base_url(), expected_path)
+ self.assertEqual( urlunsplit((scheme, netloc, path, None, None)), expected_url)
+
+ def test_redirect_redirectpath(self):
+ "Check that the potential __redirectpath is honored"
+ req = self.request()
+ txuuid = self.txuuid_toto_email
+ req.form['txuuid'] = txuuid
+ rpath = "toto"
+ req.form['__redirectpath'] = rpath
+ controller = self.vreg['controllers'].select('undo', req)
+ with self.assertRaises(Redirect) as cm:
+ result = controller.publish(rset=None)
+ self.assertURLPath(cm.exception.location, rpath)
+
+ def test_redirect_default(self):
+ req = self.request()
+ txuuid = self.txuuid_toto_email
+ req.form['txuuid'] = txuuid
+ req.session.data['breadcrumbs'] = [ urljoin(req.base_url(), path)
+ for path in ('tata', 'toto',)]
+ controller = self.vreg['controllers'].select('undo', req)
+ with self.assertRaises(Redirect) as cm:
+ result = controller.publish(rset=None)
+ self.assertURLPath(cm.exception.location, 'toto')
+
+
+class LoginControllerTC(CubicWebTC):
+
+ def test_login_with_dest(self):
+ req = self.request()
+ req.form = {'postlogin_path': 'elephants/babar'}
+ with self.assertRaises(Redirect) as cm:
+ self.ctrl_publish(req, ctrl='login')
+ self.assertEqual(req.build_url('elephants/babar'), cm.exception.location)
+
+ def test_login_no_dest(self):
+ req = self.request()
+ with self.assertRaises(Redirect) as cm:
+ self.ctrl_publish(req, ctrl='login')
+ self.assertEqual(req.base_url(), cm.exception.location)
+
if __name__ == '__main__':
unittest_main()
--- a/web/test/unittest_views_basetemplates.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_views_basetemplates.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -35,6 +35,13 @@
self.set_option('allow-email-login', 'no')
self.assertEqual(self._login_labels(), ['login', 'password'])
+
+class MainNoTopTemplateTC(CubicWebTC):
+
+ def test_valid_xhtml(self):
+ self.view('index', template='main-no-top')
+
+
if __name__ == '__main__':
from logilab.common.testlib import unittest_main
unittest_main()
--- a/web/test/unittest_views_searchrestriction.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_views_searchrestriction.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_views_staticcontrollers.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,97 @@
+from __future__ import with_statement
+
+from logilab.common.testlib import tag, Tags
+from cubicweb.devtools.testlib import CubicWebTC
+
+import os
+import os.path as osp
+import glob
+
+from cubicweb.utils import HTMLHead
+from cubicweb.web.views.staticcontrollers import ConcatFilesHandler
+
+class StaticControllerCacheTC(CubicWebTC):
+
+ tags = CubicWebTC.tags | Tags('static_controller', 'cache', 'http')
+
+
+ def _publish_static_files(self, url, header={}):
+ req = self.request(headers=header)
+ req._url = url
+ return self.app_handle_request(req, url), req
+
+ def test_static_file_are_cached(self):
+ _, req = self._publish_static_files('data/cubicweb.css')
+ self.assertEqual(200, req.status_out)
+ self.assertIn('last-modified', req.headers_out)
+ next_headers = {
+ 'if-modified-since': req.get_response_header('last-modified', raw=True),
+ }
+ _, req = self._publish_static_files('data/cubicweb.css', next_headers)
+ self.assertEqual(304, req.status_out)
+
+
+class ConcatFilesTC(CubicWebTC):
+
+ tags = CubicWebTC.tags | Tags('static_controller', 'concat')
+
+ def tearDown(self):
+ super(ConcatFilesTC, self).tearDown()
+ self._cleanup_concat_cache()
+
+ def _cleanup_concat_cache(self):
+ uicachedir = osp.join(self.config.apphome, 'uicache')
+ for fname in glob.glob(osp.join(uicachedir, 'cache_concat_*')):
+ os.unlink(osp.join(uicachedir, fname))
+
+ def _publish_js_files(self, js_files):
+ req = self.request()
+ head = HTMLHead(req)
+ url = head.concat_urls([req.data_url(js_file) for js_file in js_files])[len(req.base_url()):]
+ req._url = url
+ return self.app_handle_request(req, url), req
+
+ def expected_content(self, js_files):
+ content = u''
+ for js_file in js_files:
+ dirpath, rid = self.config.locate_resource(js_file)
+ if dirpath is not None: # ignore resources not found
+ with open(osp.join(dirpath, rid)) as f:
+ content += f.read() + '\n'
+ return content
+
+ def test_cache(self):
+ js_files = ('cubicweb.ajax.js', 'jquery.js')
+ result, req = self._publish_js_files(js_files)
+ self.assertNotEqual(404, req.status_out)
+ # check result content
+ self.assertEqual(result, self.expected_content(js_files))
+ # make sure we kept a cached version on filesystem
+ concat_hander = ConcatFilesHandler(self.config)
+ filepath = concat_hander.build_filepath(js_files)
+ self.assertTrue(osp.isfile(filepath))
+
+
+ def test_invalid_file_in_debug_mode(self):
+ js_files = ('cubicweb.ajax.js', 'dummy.js')
+ # in debug mode, an error is raised
+ self.config.debugmode = True
+ try:
+ result, req = self._publish_js_files(js_files)
+ #print result
+ self.assertEqual(404, req.status_out)
+ finally:
+ self.config.debugmode = False
+
+ def test_invalid_file_in_production_mode(self):
+ js_files = ('cubicweb.ajax.js', 'dummy.js')
+ result, req = self._publish_js_files(js_files)
+ self.assertNotEqual(404, req.status_out)
+ # check result content
+ self.assertEqual(result, self.expected_content(js_files))
+
+
+if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
+ unittest_main()
+
--- a/web/test/unittest_viewselector.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/test/unittest_viewselector.py Tue Oct 23 15:00:53 2012 +0200
@@ -30,7 +30,7 @@
primary, baseviews, tableview, editforms, calendar, management, embedding,
actions, startup, cwuser, schema, xbel, vcard, owl, treeview, idownloadable,
wdoc, debug, cwuser, cwproperties, cwsources, workflow, xmlrss, rdf,
- csvexport, json)
+ csvexport, json, undohistory)
from cubes.folder import views as folderviews
@@ -102,6 +102,7 @@
('siteinfo', debug.SiteInfoView),
('systempropertiesform', cwproperties.SystemCWPropertiesForm),
('tree', folderviews.FolderTreeView),
+ ('undohistory', undohistory.UndoHistoryView),
])
def test_possible_views_noresult(self):
--- a/web/views/actions.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/actions.py Tue Oct 23 15:00:53 2012 +0200
@@ -82,6 +82,18 @@
return 1
return 0
+class has_undoable_transactions(EntityPredicate):
+ "Select entities having public (i.e. end-user) undoable transactions."
+
+ def score_entity(self, entity):
+ if not entity._cw.vreg.config['undo-enabled']:
+ return 0
+ if entity._cw.cnx.undoable_transactions(eid=entity.eid):
+ return 1
+ else:
+ return 0
+
+
# generic 'main' actions #######################################################
class SelectAction(action.Action):
@@ -420,6 +432,7 @@
self._cw.add_js('cubicweb.rhythm.js')
return 'rhythm'
+
## default actions ui configuration ###########################################
addmenu = uicfg.actionbox_appearsin_addmenu
--- a/web/views/ajaxcontroller.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/ajaxcontroller.py Tue Oct 23 15:00:53 2012 +0200
@@ -19,7 +19,7 @@
# (disable pylint msg for client obj access to protected member as in obj._cw)
# pylint: disable=W0212
"""The ``ajaxcontroller`` module defines the :class:`AjaxController`
-controller and the ``ajax-funcs`` cubicweb registry.
+controller and the ``ajax-func`` cubicweb registry.
.. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxController
:members:
@@ -28,7 +28,7 @@
functions that can be called from the javascript world.
To register a new remote function, either decorate your function
-with the :ref:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator:
+with the :func:`~cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator:
.. sourcecode:: python
@@ -39,7 +39,7 @@
def list_users(self):
return [u for (u,) in self._cw.execute('Any L WHERE U login L')]
-or inherit from :class:`cubicwbe.web.views.ajaxcontroller.AjaxFunction` and
+or inherit from :class:`~cubicweb.web.views.ajaxcontroller.AjaxFunction` and
implement the ``__call__`` method:
.. sourcecode:: python
@@ -63,6 +63,7 @@
__docformat__ = "restructuredtext en"
+from warnings import warn
from functools import partial
from logilab.common.date import strptime
@@ -114,22 +115,20 @@
fname = self._cw.form['fname']
except KeyError:
raise RemoteCallFailed('no method specified')
+ # 1/ check first for old-style (JSonController) ajax func for bw compat
try:
- func = self._cw.vreg['ajax-func'].select(fname, self._cw)
- except ObjectNotFound:
- # function not found in the registry, inspect JSonController for
- # backward compatibility
+ func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
+ func = partial(func, self)
+ except AttributeError:
+ # 2/ check for new-style (AjaxController) ajax func
try:
- func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
- func = partial(func, self)
- except AttributeError:
+ func = self._cw.vreg['ajax-func'].select(fname, self._cw)
+ except ObjectNotFound:
raise RemoteCallFailed('no %s method' % fname)
- else:
- self.warning('remote function %s found on JSonController, '
- 'use AjaxFunction / @ajaxfunc instead', fname)
- except NoSelectableObject:
- raise RemoteCallFailed('method %s not available in this context'
- % fname)
+ else:
+ warn('[3.15] remote function %s found on JSonController, '
+ 'use AjaxFunction / @ajaxfunc instead' % fname,
+ DeprecationWarning, stacklevel=2)
# no <arg> attribute means the callback takes no argument
args = self._cw.form.get('arg', ())
if not isinstance(args, (list, tuple)):
@@ -283,11 +282,21 @@
if data is None:
raise RemoteCallFailed(self._cw._('pageid-not-found'))
return self.serialize(implementation(self, *args, **kwargs))
+
AnAjaxFunc.__name__ = implementation.__name__
# make sure __module__ refers to the original module otherwise
# vreg.register(obj) will ignore ``obj``.
AnAjaxFunc.__module__ = implementation.__module__
- return AnAjaxFunc
+ # relate the ``implementation`` object to its wrapper appobject
+ # will be used by e.g.:
+ # import base_module
+ # @ajaxfunc
+ # def foo(self):
+ # return 42
+ # assert foo(object) == 42
+ # vreg.register_and_replace(foo, base_module.older_foo)
+ implementation.__appobject__ = AnAjaxFunc
+ return implementation
def ajaxfunc(implementation=None, selector=yes(), output_type=None,
--- a/web/views/authentication.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/authentication.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/web/views/basecomponents.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/basecomponents.py Tue Oct 23 15:00:53 2012 +0200
@@ -59,6 +59,14 @@
# display multilines query as one line
rql = rset is not None and rset.printable_rql(encoded=False) or req.form.get('rql', '')
rql = rql.replace(u"\n", u" ")
+ rql_suggestion_comp = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw)
+ if rql_suggestion_comp is not None:
+ # enable autocomplete feature only if the rql
+ # suggestions builder is available
+ self._cw.add_css('jquery.ui.css')
+ self._cw.add_js(('cubicweb.ajax.js', 'jquery.ui.js'))
+ 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" />
''' % (not self.cw_propval('visible') and 'hidden' or '',
@@ -77,10 +85,10 @@
__abstract__ = True
cw_property_defs = component.override_ctx(
component.CtxComponent,
- vocabulary=['header-left', 'header-right'])
+ vocabulary=['header-center', 'header-left', 'header-right', ])
# don't want user to hide this component using an cwproperty
site_wide = True
- context = _('header-left')
+ context = _('header-center')
class ApplLogo(HeaderComponent):
@@ -88,6 +96,7 @@
__regid__ = 'logo'
__select__ = yes() # no need for a cnx
order = -1
+ context = _('header-left')
def render(self, w):
w(u'<a href="%s"><img id="logo" src="%s" alt="logo"/></a>'
--- a/web/views/basecontrollers.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/basecontrollers.py Tue Oct 23 15:00:53 2012 +0200
@@ -27,14 +27,14 @@
from logilab.common.deprecation import deprecated
from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
- AuthenticationError, typed_eid)
+ AuthenticationError, typed_eid, UndoTransactionException)
from cubicweb.utils import json_dumps
from cubicweb.predicates import (authenticated_user, anonymous_user,
match_form_params)
from cubicweb.web import Redirect, RemoteCallFailed
-from cubicweb.web.controller import Controller
+from cubicweb.web.controller import Controller, append_url_params
from cubicweb.web.views import vid_from_rset
-
+import cubicweb.transaction as tx
@deprecated('[3.15] jsonize is deprecated, use AjaxFunction appobjects instead')
def jsonize(func):
@@ -84,6 +84,17 @@
# Cookie authentication
return self.appli.need_login_content(self._cw)
+class LoginControllerForAuthed(Controller):
+ __regid__ = 'login'
+ __select__ = ~anonymous_user()
+
+ def publish(self, rset=None):
+ """log in the instance"""
+ path = self._cw.form.get('postlogin_path', '')
+ # redirect expect an url, not a path. Also path may contains a query
+ # string, hence should not be given to _cw.build_url()
+ raise Redirect(self._cw.base_url() + path)
+
class LogoutController(Controller):
__regid__ = 'logout'
@@ -179,6 +190,7 @@
def _validation_error(req, ex):
req.cnx.rollback()
+ ex.tr(req._) # translate messages using ui language
# XXX necessary to remove existant validation error?
# imo (syt), it's not necessary
req.session.data.pop(req.form.get('__errorurl'), None)
@@ -203,7 +215,7 @@
return (False, _validation_error(req, ex), ctrl._edited_entity)
except Redirect, ex:
try:
- req.cnx.commit() # ValidationError may be raise on commit
+ txuuid = req.cnx.commit() # ValidationError may be raised on commit
except ValidationError, ex:
return (False, _validation_error(req, ex), ctrl._edited_entity)
except Exception, ex:
@@ -211,6 +223,8 @@
req.exception('unexpected error while validating form')
return (False, str(ex).decode('utf-8'), ctrl._edited_entity)
else:
+ if txuuid is not None:
+ req.data['last_undoable_transaction'] = txuuid
# complete entity: it can be used in js callbacks where we might
# want every possible information
if ctrl._edited_entity:
@@ -275,17 +289,17 @@
def publish(self, rset=None):
txuuid = self._cw.form['txuuid']
- errors = self._cw.cnx.undo_transaction(txuuid)
- if not errors:
- self.redirect()
- raise ValidationError(None, {None: '\n'.join(errors)})
+ try:
+ self._cw.cnx.undo_transaction(txuuid)
+ except UndoTransactionException, exc:
+ errors = exc.errors
+ #This will cause a rollback in main_publish
+ raise ValidationError(None, {None: '\n'.join(errors)})
+ else :
+ self.redirect() # Will raise Redirect
def redirect(self, msg=None):
req = self._cw
msg = msg or req._("transaction undone")
- breadcrumbs = req.session.data.get('breadcrumbs', None)
- if breadcrumbs is not None and len(breadcrumbs) > 1:
- url = req.rebuild_url(breadcrumbs[-2], __message=msg)
- else:
- url = req.build_url(__message=msg)
- raise Redirect(url)
+ self._return_to_lastpage( dict(_cwmsgid= req.set_redirect_message(msg)) )
+
--- a/web/views/basetemplates.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/basetemplates.py Tue Oct 23 15:00:53 2012 +0200
@@ -23,6 +23,7 @@
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.view import View, MainTemplate, NOINDEX, NOFOLLOW, StartupView
@@ -256,10 +257,10 @@
whead(u'\n'.join(additional_headers) + u'\n')
self.wview('htmlheader', rset=self.cw_rset)
w = self.w
- w(u'<title>%s</title>\n' % xml_escape(page_title))
+ whead(u'<title>%s</title>\n' % xml_escape(page_title))
w(u'<body>\n')
w(u'<div id="page">')
- w(u'<table width="100%" height="100%" border="0"><tr>\n')
+ w(u'<table width="100%" border="0" id="mainLayout"><tr>\n')
w(u'<td id="navColumnLeft">\n')
self.topleft_header()
boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
@@ -270,11 +271,7 @@
box.render(w=w)
self.w(u'</div>\n')
w(u'</td>')
- w(u'<td id="contentcol" rowspan="2">')
- w(u'<div id="pageContent">\n')
- vtitle = self._cw.form.get('vtitle')
- if vtitle:
- w(u'<div class="vtitle">%s</div>' % xml_escape(vtitle))
+ w(u'<td id="contentColumn" rowspan="2">')
def topleft_header(self):
logo = self._cw.vreg['components'].select_or_none('logo', self._cw,
@@ -332,7 +329,9 @@
__regid__ = 'header'
main_cell_components = ('appliname', 'breadcrumbs')
headers = (('headtext', 'header-left'),
- ('header-right', 'header-right'))
+ ('header-center', 'header-center'),
+ ('header-right', 'header-right')
+ )
def call(self, view, **kwargs):
self.main_header(view)
@@ -421,26 +420,56 @@
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
+ """
+ __abstract__ = True
-class LogForm(forms.FieldsForm):
__regid__ = 'logform'
domid = 'loginForm'
needs_css = ('cubicweb.login.css',)
- onclick = "javascript: cw.htmlhelpers.popupLoginBox('%s', '%s');"
+
+ onclick_base = "javascript: cw.htmlhelpers.popupLoginBox('%s', '%s');"
+ onclick_args = (None, None)
+
+ @classproperty
+ def form_buttons(cls):
+ # we use a property because sub class will need to define their own onclick_args.
+ # Therefor we can't juste make the string formating when instanciating this class
+ onclick = cls.onclick_base % cls.onclick_args
+ form_buttons = [fw.SubmitButton(label=_('log in'),
+ attrs={'class': 'loginButton'}),
+ fw.ResetButton(label=_('cancel'),
+ attrs={'class': 'loginButton',
+ 'onclick': onclick}),]
+ ## 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
+
+ def form_action(self):
+ if self.action is None:
+ # reuse existing redirection if it exists
+ target = self._cw.form.get('postlogin_path',
+ self._cw.relative_path())
+ url_args = {}
+ if target and target != '/':
+ url_args['postlogin_path'] = target
+ return self._cw.build_url('login', __secure__=True, **url_args)
+ return super(LogForm, self).form_action()
+
+class LogForm(BaseLogForm):
+ """Simple login form that send username and password
+ """
+ __regid__ = 'logform'
+ domid = 'loginForm'
+ needs_css = ('cubicweb.login.css',)
# XXX have to recall fields name since python is mangling __login/__password
__login = ff.StringField('__login', widget=fw.TextInput({'class': 'data'}))
__password = ff.StringField('__password', label=_('password'),
widget=fw.PasswordSingleInput({'class': 'data'}))
- form_buttons = [fw.SubmitButton(label=_('log in'),
- attrs={'class': 'loginButton'}),
- fw.ResetButton(label=_('cancel'),
- attrs={'class': 'loginButton',
- 'onclick': onclick % ('popupLoginBox', '__login')}),]
- def form_action(self):
- if self.action is None:
- return login_form_url(self._cw)
- return super(LogForm, self).form_action()
+ onclick_args = ('popupLoginBox', '__login')
class LogFormView(View):
@@ -486,12 +515,3 @@
cw.html_headers.add_onload('jQuery("#__login:visible").focus()')
LogFormTemplate = class_renamed('LogFormTemplate', LogFormView)
-
-
-def login_form_url(req):
- if req.https:
- return req.url()
- httpsurl = req.vreg.config.get('https-url')
- if httpsurl:
- return req.url().replace(req.base_url(), httpsurl)
- return req.url()
--- a/web/views/baseviews.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/baseviews.py Tue Oct 23 15:00:53 2012 +0200
@@ -173,9 +173,9 @@
class OutOfContextView(EntityView):
""":__regid__: *outofcontext*
- This view is used whenthe entity should be considered as displayed out of
- its context. By default it produces the result of ``entity.dc_long_title()`` wrapped
- in a link leading to the primary view of the entity.
+ This view is used when the entity should be considered as displayed out of
+ its context. By default it produces the result of ``entity.dc_long_title()``
+ wrapped in a link leading to the primary view of the entity.
"""
__regid__ = 'outofcontext'
@@ -612,18 +612,18 @@
def group_key(self, entity, **kwargs):
value = super(AuthorView, self).group_key(entity, **kwargs)
if value:
- return value.login
- return value
+ return (value.name(), value.login)
+ return (None, None)
def index_link(self, basepath, key, items):
- label = u'%s [%s]' % (key, len(items))
+ label = u'%s [%s]' % (key[0], len(items))
etypes = set(entity.__regid__ for entity in items)
vtitle = self._cw._('%(etype)s by %(author)s') % {
'etype': ', '.join(display_name(self._cw, etype, 'plural')
for etype in etypes),
'author': label}
- url = self.index_url(basepath, key, vtitle=vtitle)
- title = self._cw._('archive for %(author)s') % {'author': key}
+ url = self.index_url(basepath, key[1], vtitle=vtitle)
+ title = self._cw._('archive for %(author)s') % {'author': key[0]}
return tags.a(label, href=url, title=title)
--- a/web/views/boxes.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/boxes.py Tue Oct 23 15:00:53 2012 +0200
@@ -48,17 +48,17 @@
BoxTemplate = box.BoxTemplate
BoxHtml = htmlwidgets.BoxHtml
-class EditBox(component.CtxComponent): # XXX rename to ActionsBox
+class EditBox(component.CtxComponent):
"""
box with all actions impacting the entity displayed: edit, copy, delete
change state, add related entities...
"""
__regid__ = 'edit_box'
- __select__ = component.CtxComponent.__select__ & non_final_entity()
title = _('actions')
order = 2
contextual = True
+ __select__ = component.CtxComponent.__select__ & non_final_entity()
def init_rendering(self):
super(EditBox, self).init_rendering()
--- a/web/views/calendar.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/calendar.py Tue Oct 23 15:00:53 2012 +0200
@@ -178,42 +178,31 @@
fullcalendar_options = {
'firstDay': 1,
+ 'firstHour': 8,
+ 'defaultView': 'month',
+ 'editable': True,
'header': {'left': 'prev,next today',
'center': 'title',
'right': 'month,agendaWeek,agendaDay',
},
- 'editable': True,
- 'defaultView': 'month',
- 'timeFormat': {'month': '',
- '': 'H:mm'},
- 'firstHour': 8,
- 'axisFormat': 'H:mm',
- 'columnFormat': {'month': 'dddd',
- 'agendaWeek': 'dddd yyyy/M/dd',
- 'agendaDay': 'dddd yyyy/M/dd'}
}
-
def call(self):
self._cw.demote_to_html()
self._cw.add_css(('fullcalendar.css', 'cubicweb.calendar.css'))
- self._cw.add_js(('jquery.ui.js', 'fullcalendar.min.js', 'jquery.qtip.min.js'))
+ self._cw.add_js(('jquery.ui.js', 'fullcalendar.min.js', 'jquery.qtip.min.js', 'fullcalendar.locale.js'))
self.calendar_id = 'cal' + make_uid('uid')
self.add_onload()
# write calendar div to load jquery fullcalendar object
self.w(u'<div id="%s"></div>' % self.calendar_id)
-
def add_onload(self):
fullcalendar_options = self.fullcalendar_options.copy()
fullcalendar_options['events'] = self.get_events()
- fullcalendar_options['buttonText'] = {'today': self._cw._('today'),
- 'month': self._cw._('month'),
- 'week': self._cw._('week'),
- 'day': self._cw._('day')}
+ # i18n
# js callback to add a tooltip and to put html in event's title
js = """
- var options = %s;
+ var options = $.fullCalendar.regional('%s', %s);
options.eventRender = function(event, $element) {
// add a tooltip for each event
var div = '<div class="tooltip">'+ event.description+ '</div>';
@@ -223,8 +212,7 @@
};
$("#%s").fullCalendar(options);
""" #"
- self._cw.add_onload(js % (json_dumps(fullcalendar_options), self.calendar_id))
-
+ self._cw.add_onload(js % (self._cw.lang, json_dumps(fullcalendar_options), self.calendar_id))
def get_events(self):
events = []
--- a/web/views/cwsources.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/cwsources.py Tue Oct 23 15:00:53 2012 +0200
@@ -373,8 +373,9 @@
w(u'<label>%s</label>' % self._cw._(u'Message threshold'))
w(u'<select class="log_filter" onchange="filterLog(\'%s\', this.options[this.selectedIndex].value)">'
% self.view.domid)
- for level in ('Debug', 'Info', 'Warning', 'Error', 'Fatal'):
- w('<option value="%s">%s</option>' % (level, self._cw._(level)))
+ for level in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'FATAL'):
+ w('<option value="%s">%s</option>' % (level.capitalize(),
+ self._cw._(level)))
w(u'</select>')
w(u'</fieldset></form>')
super(LogTableLayout, self).render_table(w, actions, paginate)
@@ -421,7 +422,9 @@
class URLRenderer(pyviews.PyValTableColRenderer):
def render_cell(self, w, rownum):
url = self.data[rownum][1]
- w(url and tags.a(url, href=url) or u' ')
+ if url and url.startswith('http'):
+ url = tags.a(url, href=url)
+ w(url or u' ')
class LineRenderer(pyviews.PyValTableColRenderer):
def render_cell(self, w, rownum):
--- a/web/views/editcontroller.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/editcontroller.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/web/views/facets.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/facets.py Tue Oct 23 15:00:53 2012 +0200
@@ -26,6 +26,7 @@
from logilab.common.decorators import cachedproperty
from logilab.common.registry import objectify_predicate, yes
+from cubicweb import tags
from cubicweb.predicates import (non_final_entity, multi_lines_rset,
match_context_prop, relation_possible)
from cubicweb.utils import json_dumps
@@ -234,6 +235,7 @@
vid = req.form.get('vid')
if self.bk_linkbox_template and req.vreg.schema['Bookmark'].has_perm(req, 'add'):
w(self.bookmark_link(rset))
+ w(self.focus_link(rset))
hiddens = {}
for param in ('subvid', 'vtitle'):
if param in req.form:
@@ -269,6 +271,9 @@
req._('bookmark this search'))
return self.bk_linkbox_template % bk_link
+ def focus_link(self, rset):
+ return self.bk_linkbox_template % tags.a(self._cw._('focus on this selection'),
+ href=self._cw.url(), id='focusLink')
class FilterTable(FacetFilterMixIn, AnyRsetView):
__regid__ = 'facet.filtertable'
--- a/web/views/formrenderers.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/formrenderers.py Tue Oct 23 15:00:53 2012 +0200
@@ -117,7 +117,9 @@
errormsg = self.error_message(form)
if errormsg:
data.insert(0, errormsg)
- w(''.join(data))
+ # NOTE: we call unicode because `tag` objects may be found within data
+ # e.g. from the cwtags library
+ w(''.join(unicode(x) for x in data))
def render_content(self, w, form, values):
if self.display_progress_div:
@@ -492,9 +494,30 @@
entity's form.
"""
__regid__ = 'inline'
+ fieldset_css_class = 'subentity'
+
+ def render_title(self, w, form, values):
+ w(u'<div class="iformTitle">')
+ w(u'<span>%(title)s</span> '
+ '#<span class="icounter">%(counter)s</span> ' % values)
+ if values['removejs']:
+ values['removemsg'] = self._cw._('remove-inlined-entity-form')
+ w(u'[<a href="javascript: %(removejs)s;$.noop();">%(removemsg)s</a>]'
+ % values)
+ w(u'</div>')
def render(self, w, form, values):
form.add_media()
+ self.open_form(w, form, values)
+ self.render_title(w, form, values)
+ # XXX that stinks
+ # cleanup values
+ for key in ('title', 'removejs', 'removemsg'):
+ values.pop(key, None)
+ self.render_fields(w, form, values)
+ self.close_form(w, form, values)
+
+ def open_form(self, w, form, values):
try:
w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">' % values)
except KeyError:
@@ -503,29 +526,15 @@
w(u'<div id="notice-%s" class="notice">%s</div>' % (
values['divid'], self._cw._('click on the box to cancel the deletion')))
w(u'<div class="iformBody">')
- eschema = form.edited_entity.e_schema
- if values['removejs']:
- values['removemsg'] = self._cw._('remove-inlined-entity-form')
- w(u'<div class="iformTitle"><span>%(title)s</span> '
- '#<span class="icounter">%(counter)s</span> '
- '[<a href="javascript: %(removejs)s;$.noop();">%(removemsg)s</a>]</div>'
- % values)
- else:
- w(u'<div class="iformTitle"><span>%(title)s</span> '
- '#<span class="icounter">%(counter)s</span></div>'
- % values)
- # XXX that stinks
- # cleanup values
- for key in ('title', 'removejs', 'removemsg'):
- values.pop(key, None)
- self.render_fields(w, form, values)
+
+ def close_form(self, w, form, values):
w(u'</div></div>')
def render_fields(self, w, form, values):
w(u'<fieldset id="fs-%(divid)s">' % values)
fields = self._render_hidden_fields(w, form)
w(u'</fieldset>')
- w(u'<fieldset class="subentity">')
+ w(u'<fieldset class="%s">' % self.fieldset_css_class)
if fields:
self._render_fields(fields, w, form)
self.render_child_forms(w, form, values)
--- a/web/views/magicsearch.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/magicsearch.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,19 +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/>.
-"""a query processor to handle quick search shortcuts for cubicweb"""
+"""a query processor to handle quick search shortcuts for cubicweb
+"""
__docformat__ = "restructuredtext en"
import re
from logging import getLogger
-from warnings import warn
+
+from yams.interfaces import IVocabularyConstraint
from rql import RQLSyntaxError, BadRQLQuery, parse
+from rql.utils import rqlvar_maker
from rql.nodes import Relation
from cubicweb import Unauthorized, typed_eid
from cubicweb.view import Component
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
LOGGER = getLogger('cubicweb.magicsearch')
@@ -408,3 +412,247 @@
# explicitly specified processor: don't try to catch the exception
return proc.process_query(uquery)
raise BadRQLQuery(self._cw._('sorry, the server is unable to handle this query'))
+
+
+
+## RQL suggestions builder ####################################################
+class RQLSuggestionsBuilder(Component):
+ """main entry point is `build_suggestions()` which takes
+ an incomplete RQL query and returns a list of suggestions to complete
+ the query.
+
+ This component is enabled by default and is used to provide autocompletion
+ in the RQL search bar. If you don't want this feature in your application,
+ just unregister it or make it unselectable.
+
+ .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.build_suggestions
+ .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.etypes_suggestion_set
+ .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_etypes
+ .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_relations
+ .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.vocabulary
+ """
+ __regid__ = 'rql.suggestions'
+
+ #: maximum number of results to fetch when suggesting attribute values
+ attr_value_limit = 20
+
+ def build_suggestions(self, user_rql):
+ """return a list of suggestions to complete `user_rql`
+
+ :param user_rql: an incomplete RQL query
+ """
+ req = self._cw
+ try:
+ if 'WHERE' not in user_rql: # don't try to complete if there's no restriction
+ return []
+ variables, restrictions = [part.strip() for part in user_rql.split('WHERE', 1)]
+ if ',' in restrictions:
+ restrictions, incomplete_part = restrictions.rsplit(',', 1)
+ user_rql = '%s WHERE %s' % (variables, restrictions)
+ else:
+ restrictions, incomplete_part = '', restrictions
+ user_rql = variables
+ select = parse(user_rql, print_errors=False).children[0]
+ req.vreg.rqlhelper.annotate(select)
+ req.vreg.solutions(req, select, {})
+ if restrictions:
+ return ['%s, %s' % (user_rql, suggestion)
+ for suggestion in self.rql_build_suggestions(select, incomplete_part)]
+ else:
+ return ['%s WHERE %s' % (user_rql, suggestion)
+ for suggestion in self.rql_build_suggestions(select, incomplete_part)]
+ except Exception, exc: # we never want to crash
+ self.debug('failed to build suggestions: %s', exc)
+ return []
+
+ ## actual completion entry points #########################################
+ def rql_build_suggestions(self, select, incomplete_part):
+ """
+ :param select: the annotated select node (rql syntax tree)
+ :param incomplete_part: the part of the rql query that needs
+ to be completed, (e.g. ``X is Pr``, ``X re``)
+ """
+ chunks = incomplete_part.split(None, 2)
+ if not chunks: # nothing to complete
+ return []
+ if len(chunks) == 1: # `incomplete` looks like "MYVAR"
+ return self._complete_rqlvar(select, *chunks)
+ elif len(chunks) == 2: # `incomplete` looks like "MYVAR some_rel"
+ return self._complete_rqlvar_and_rtype(select, *chunks)
+ elif len(chunks) == 3: # `incomplete` looks like "MYVAR some_rel something"
+ return self._complete_relation_object(select, *chunks)
+ else: # would be anything else, hard to decide what to do here
+ return []
+
+ # _complete_* methods are considered private, at least while the API
+ # isn't stabilized.
+ def _complete_rqlvar(self, select, rql_var):
+ """return suggestions for "variable only" incomplete_part
+
+ as in :
+
+ - Any X WHERE X
+ - Any X WHERE X is Project, Y
+ - etc.
+ """
+ return ['%s %s %s' % (rql_var, rtype, dest_var)
+ for rtype, dest_var in self.possible_relations(select, rql_var)]
+
+ def _complete_rqlvar_and_rtype(self, select, rql_var, user_rtype):
+ """return suggestions for "variable + rtype" incomplete_part
+
+ as in :
+
+ - Any X WHERE X is
+ - Any X WHERE X is Person, X firstn
+ - etc.
+ """
+ # special case `user_type` == 'is', return every possible type.
+ if user_rtype == 'is':
+ return self._complete_is_relation(select, rql_var)
+ else:
+ return ['%s %s %s' % (rql_var, rtype, dest_var)
+ for rtype, dest_var in self.possible_relations(select, rql_var)
+ if rtype.startswith(user_rtype)]
+
+ def _complete_relation_object(self, select, rql_var, user_rtype, user_value):
+ """return suggestions for "variable + rtype + some_incomplete_value"
+
+ as in :
+
+ - Any X WHERE X is Per
+ - Any X WHERE X is Person, X firstname "
+ - Any X WHERE X is Person, X firstname "Pa
+ - etc.
+ """
+ # special case `user_type` == 'is', return every possible type.
+ if user_rtype == 'is':
+ return self._complete_is_relation(select, rql_var, user_value)
+ elif user_value:
+ if user_value[0] in ('"', "'"):
+ # if finished string, don't suggest anything
+ if len(user_value) > 1 and user_value[-1] == user_value[0]:
+ return []
+ user_value = user_value[1:]
+ return ['%s %s "%s"' % (rql_var, user_rtype, value)
+ for value in self.vocabulary(select, rql_var,
+ user_rtype, user_value)]
+ return []
+
+ def _complete_is_relation(self, select, rql_var, prefix=''):
+ """return every possible types for rql_var
+
+ :param prefix: if specified, will only return entity types starting
+ with the specified value.
+ """
+ return ['%s is %s' % (rql_var, etype)
+ for etype in self.possible_etypes(select, rql_var, prefix)]
+
+ def etypes_suggestion_set(self):
+ """returns the list of possible entity types to suggest
+
+ The default is to return any non-final entity type available
+ in the schema.
+
+ Can be overridden for instance if an application decides
+ to restrict this list to a meaningful set of business etypes.
+ """
+ schema = self._cw.vreg.schema
+ return set(eschema.type for eschema in schema.entities() if not eschema.final)
+
+ def possible_etypes(self, select, rql_var, prefix=''):
+ """return all possible etypes for `rql_var`
+
+ The returned list will always be a subset of meth:`etypes_suggestion_set`
+
+ :param select: the annotated select node (rql syntax tree)
+ :param rql_var: the variable name for which we want to know possible types
+ :param prefix: if specified, will only return etypes starting with it
+ """
+ available_etypes = self.etypes_suggestion_set()
+ possible_etypes = set()
+ for sol in select.solutions:
+ if rql_var in sol and sol[rql_var] in available_etypes:
+ possible_etypes.add(sol[rql_var])
+ if not possible_etypes:
+ # `Any X WHERE X is Person, Y is`
+ # -> won't have a solution, need to give all etypes
+ possible_etypes = available_etypes
+ return sorted(etype for etype in possible_etypes if etype.startswith(prefix))
+
+ def possible_relations(self, select, rql_var, include_meta=False):
+ """returns a list of couple (rtype, dest_var) for each possible
+ relations with `rql_var` as subject.
+
+ ``dest_var`` will be picked among availabel variables if types match,
+ otherwise a new one will be created.
+ """
+ schema = self._cw.vreg.schema
+ relations = set()
+ untyped_dest_var = rqlvar_maker(defined=select.defined_vars).next()
+ # for each solution
+ # 1. find each possible relation
+ # 2. for each relation:
+ # 2.1. if the relation is meta, skip it
+ # 2.2. for each possible destination type, pick up possible
+ # variables for this type or use a new one
+ for sol in select.solutions:
+ etype = sol[rql_var]
+ sol_by_types = {}
+ for varname, var_etype in sol.items():
+ # don't push subject var to avoid "X relation X" suggestion
+ if varname != rql_var:
+ sol_by_types.setdefault(var_etype, []).append(varname)
+ for rschema in schema[etype].subject_relations():
+ if include_meta or not rschema.meta:
+ for dest in rschema.objects(etype):
+ for varname in sol_by_types.get(dest.type, (untyped_dest_var,)):
+ suggestion = (rschema.type, varname)
+ if suggestion not in relations:
+ relations.add(suggestion)
+ return sorted(relations)
+
+ def vocabulary(self, select, rql_var, user_rtype, rtype_incomplete_value):
+ """return acceptable vocabulary for `rql_var` + `user_rtype` in `select`
+
+ Vocabulary is either found from schema (Yams) definition or
+ directly from database.
+ """
+ schema = self._cw.vreg.schema
+ vocab = []
+ for sol in select.solutions:
+ # for each solution :
+ # - If a vocabulary constraint exists on `rql_var+user_rtype`, use it
+ # to define possible values
+ # - Otherwise, query the database to fetch available values from
+ # database (limiting results to `self.attr_value_limit`)
+ try:
+ eschema = schema.eschema(sol[rql_var])
+ rdef = eschema.rdef(user_rtype)
+ except KeyError: # unknown relation
+ continue
+ cstr = rdef.constraint_by_interface(IVocabularyConstraint)
+ if cstr is not None:
+ # a vocabulary is found, use it
+ vocab += [value for value in cstr.vocabulary()
+ if value.startswith(rtype_incomplete_value)]
+ elif rdef.final:
+ # no vocab, query database to find possible value
+ vocab_rql = 'DISTINCT Any V LIMIT %s WHERE X is %s, X %s V' % (
+ self.attr_value_limit, eschema.type, user_rtype)
+ vocab_kwargs = {}
+ if rtype_incomplete_value:
+ vocab_rql += ', X %s LIKE %%(value)s' % user_rtype
+ vocab_kwargs['value'] = '%s%%' % rtype_incomplete_value
+ vocab += [value for value, in
+ self._cw.execute(vocab_rql, vocab_kwargs)]
+ return sorted(set(vocab))
+
+
+
+@ajaxfunc(output_type='json')
+def rql_suggest(self):
+ rql_builder = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw)
+ if rql_builder:
+ return rql_builder.build_suggestions(self._cw.form['term'])
+ return []
--- a/web/views/navigation.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/navigation.py Tue Oct 23 15:00:53 2012 +0200
@@ -364,11 +364,13 @@
@property
def prev_icon(self):
- return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_prev.png'))
+ return '<img src="%s" alt="%s" />' % (
+ xml_escape(self._cw.data_url('go_prev.png')), self._cw._('previous page'))
@property
def next_icon(self):
- return '<img src="%s"/>' % xml_escape(self._cw.data_url('go_next.png'))
+ return '<img src="%s" alt="%s" />' % (
+ xml_escape(self._cw.data_url('go_next.png')), self._cw._('next page'))
def init_rendering(self):
adapter = self.entity.cw_adapt_to('IPrevNext')
--- a/web/views/sessions.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/sessions.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -95,18 +95,6 @@
# reopening. Is it actually a problem?
if 'last_login_time' in req.vreg.schema:
self._update_last_login_time(req)
- args = req.form
- for forminternal_key in ('__form_id', '__domid', '__errorurl'):
- args.pop(forminternal_key, None)
- path = req.relative_path(False)
- if path in ('login', 'logout') or req.form.get('vid') == 'loggedout':
- path = 'view'
- args['__message'] = req._('welcome %s !') % req.user.login
- if 'vid' in req.form and req.form['vid'] != 'loggedout':
- args['vid'] = req.form['vid']
- if 'rql' in req.form:
- args['rql'] = req.form['rql']
- raise Redirect(req.build_url(path, **args))
req.set_message(req._('welcome %s !') % req.user.login)
def _update_last_login_time(self, req):
--- a/web/views/startup.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/startup.py Tue Oct 23 15:00:53 2012 +0200
@@ -53,6 +53,7 @@
add_etype_links = ()
skip_startup_views = set( ('index', 'manage', 'schema', 'owl', 'changelog',
'systempropertiesform', 'propertiesform',
+ 'loggedout', 'login',
'cw.users-and-groups-management', 'cw.groups-management',
'cw.users-management', 'cw.sources-management',
'siteinfo', 'info', 'registry', 'gc',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/staticcontrollers.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,250 @@
+# copyright 2003-2011 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/>.
+"""Set of static resources controllers for :
+
+- /data/...
+- /static/...
+- /fckeditor/...
+
+"""
+from __future__ import with_statement
+
+import os
+import os.path as osp
+import hashlib
+import mimetypes
+from time import mktime
+from datetime import datetime, timedelta
+from logging import getLogger
+
+from cubicweb import Unauthorized
+from cubicweb.web import NotFound
+from cubicweb.web.http_headers import generateDateTime
+from cubicweb.web.controller import Controller
+from cubicweb.web.views.urlrewrite import URLRewriter
+
+
+
+class StaticFileController(Controller):
+ """an abtract class to serve static file
+
+ Make sure to add your subclass to the STATIC_CONTROLLERS list"""
+ __abstract__ = True
+ directory_listing_allowed = False
+
+ def max_age(self, path):
+ """max cache TTL"""
+ return 60*60*24*7
+
+ def static_file(self, path):
+ """Return full content of a static file.
+
+ XXX iterable content would be better
+ """
+ debugmode = self._cw.vreg.config.debugmode
+ if osp.isdir(path):
+ if self.directory_listing_allowed:
+ return u''
+ raise Unauthorized(path)
+ if not osp.isfile(path):
+ raise NotFound()
+ if not debugmode:
+ # XXX: Don't provide additional resource information to error responses
+ #
+ # the HTTP RFC recommands not going further than 1 year ahead
+ expires = datetime.now() + timedelta(days=6*30)
+ self._cw.set_header('Expires', generateDateTime(mktime(expires.timetuple())))
+
+ # XXX system call to os.stats could be cached once and for all in
+ # production mode (where static files are not expected to change)
+ #
+ # Note that: we do a osp.isdir + osp.isfile before and a potential
+ # os.read after. Improving this specific call will not help
+ #
+ # Real production environment should use dedicated static file serving.
+ self._cw.set_header('last-modified', generateDateTime(os.stat(path).st_mtime))
+ self._cw.validate_cache()
+ # XXX elif uri.startswith('/https/'): uri = uri[6:]
+ mimetype, encoding = mimetypes.guess_type(path)
+ if mimetype is None:
+ mimetype = 'application/octet-stream'
+ self._cw.set_content_type(mimetype, osp.basename(path), encoding)
+ with open(path, 'rb') as resource:
+ return resource.read()
+
+ @property
+ def relpath(self):
+ """path of a requested file relative to the controller"""
+ path = self._cw.form.get('static_relative_path')
+ if path is None:
+ path = self._cw.relative_path(includeparams=True)
+ return path
+
+
+class ConcatFilesHandler(object):
+ """Emulating the behavior of modconcat
+
+ this serve multiple file as a single one.
+ """
+
+ def __init__(self, config):
+ self._resources = {}
+ self.config = config
+ self.logger = getLogger('cubicweb.web')
+
+ def _resource(self, path):
+ """get the resouce"""
+ try:
+ return self._resources[path]
+ except KeyError:
+ self._resources[path] = self.config.locate_resource(path)
+ return self._resources[path]
+
+ def _up_to_date(self, filepath, paths):
+ """
+ The concat-file is considered up-to-date if it exists.
+ In debug mode, an additional check is performed to make sure that
+ concat-file is more recent than all concatenated files
+ """
+ if not osp.isfile(filepath):
+ return False
+ if self.config.debugmode:
+ concat_lastmod = os.stat(filepath).st_mtime
+ for path in paths:
+ dirpath, rid = self._resource(path)
+ if rid is None:
+ raise NotFound(path)
+ path = osp.join(dirpath, rid)
+ if os.stat(path).st_mtime > concat_lastmod:
+ return False
+ return True
+
+ def build_filepath(self, paths):
+ """return the filepath that will be used to cache concatenation of `paths`
+ """
+ _, ext = osp.splitext(paths[0])
+ fname = 'cache_concat_' + hashlib.md5(';'.join(paths)).hexdigest() + ext
+ return osp.join(self.config.appdatahome, 'uicache', fname)
+
+ def concat_cached_filepath(self, paths):
+ filepath = self.build_filepath(paths)
+ if not self._up_to_date(filepath, paths):
+ with open(filepath, 'wb') as f:
+ for path in paths:
+ dirpath, rid = self._resource(path)
+ if rid is None:
+ # In production mode log an error, do not return a 404
+ # XXX the erroneous content is cached anyway
+ self.logger.error('concatenated data url error: %r file '
+ 'does not exist', path)
+ if self.config.debugmode:
+ raise NotFound(path)
+ else:
+ with open(osp.join(dirpath, rid), 'rb') as source:
+ for line in source:
+ f.write(line)
+ f.write('\n')
+ return filepath
+
+
+class DataController(StaticFileController):
+ """Controller in charge of serving static file in /data/
+
+ Handle modeconcat like url.
+ """
+
+ __regid__ = 'data'
+
+ def __init__(self, *args, **kwargs):
+ super(DataController, self).__init__(*args, **kwargs)
+ config = self._cw.vreg.config
+ md5_version = config.instance_md5_version()
+ self.base_datapath = config.data_relpath()
+ self.data_modconcat_basepath = '%s??' % self.base_datapath
+ self.concat_files_registry = ConcatFilesHandler(config)
+
+ def publish(self, rset=None):
+ config = self._cw.vreg.config
+ # includeparams=True for modconcat-like urls
+ relpath = self.relpath
+ if relpath.startswith(self.data_modconcat_basepath):
+ paths = relpath[len(self.data_modconcat_basepath):].split(',')
+ filepath = self.concat_files_registry.concat_cached_filepath(paths)
+ else:
+ # skip leading '/data/' and url params
+ relpath = relpath[len(self.base_datapath):].split('?', 1)[0]
+ dirpath, rid = config.locate_resource(relpath)
+ if dirpath is None:
+ raise NotFound()
+ filepath = osp.join(dirpath, rid)
+ return self.static_file(filepath)
+
+
+class FCKEditorController(StaticFileController):
+ """Controller in charge of serving FCKEditor related file
+
+ The motivational for a dedicated controller have been lost.
+ """
+
+ __regid__ = 'fckeditor'
+
+ def publish(self, rset=None):
+ config = self._cw.vreg.config
+ if self._cw.https:
+ uiprops = config.https_uiprops
+ else:
+ uiprops = config.uiprops
+ relpath = self.relpath
+ if relpath.startswith('fckeditor/'):
+ relpath = relpath[len('fckeditor/'):]
+ relpath = relpath.split('?', 1)[0]
+ return self.static_file(osp.join(uiprops['FCKEDITOR_PATH'], relpath))
+
+
+class StaticDirectoryController(StaticFileController):
+ """Controller in charge of serving static file in /static/
+ """
+ __regid__ = 'static'
+
+ def publish(self, rset=None):
+ staticdir = self._cw.vreg.config.static_directory
+ relpath = self.relpath
+ 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.
+
+ This is a work around the flatness of url handling in cubicweb."""
+
+ __regid__ = 'static'
+
+ priority = 10
+
+ def rewrite(self, req, uri):
+ for ctrl in STATIC_CONTROLLERS:
+ if uri.startswith('/%s/' % ctrl.__regid__):
+ break
+ 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/web/views/tableview.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/tableview.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -135,7 +135,7 @@
* `header_column_idx`, if not `None`, should be a colum index or a set of
column index where <th> tags should be generated instead of <td>
- """
+ """ #'# make emacs happier
__regid__ = 'table_layout'
cssclass = "listing"
needs_css = ('cubicweb.tableview.css',)
@@ -174,8 +174,8 @@
@cachedproperty
def initial_load(self):
- """We detect a bit heuristically if we are built for the first time.
- or from subsequent calls by the form filter or by the pagination hooks.
+ """We detect a bit heuristically if we are built for the first time or
+ from subsequent calls by the form filter or by the pagination hooks.
"""
form = self._cw.form
return 'fromformfilter' not in form and '__fromnavigation' not in form
@@ -290,20 +290,17 @@
return attrs
def render_actions(self, w, actions):
- box = MenuWidget('', '', _class='tableActionsBox', islist=False)
- label = tags.span(self._cw._('action menu'))
- menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox',
- ident='%sActions' % self.view.domid)
- box.append(menu)
+ w(u'<div class="tableactions">')
for action in actions:
- menu.append(action)
- box.render(w=w)
- w(u'<div class="clear"></div>')
+ w(u'<span>')
+ action.render(w)
+ w(u'</span>')
+ w(u'</div>')
def show_hide_filter_actions(self, currentlydisplayed=False):
divid = self.view.domid
showhide = u';'.join(toggle_action('%s%s' % (divid, what))[11:]
- for what in ('Form', 'Show', 'Hide', 'Actions'))
+ for what in ('Form', 'Actions'))
showhide = 'javascript:' + showhide
self._cw.add_onload(u'''\
$(document).ready(function() {
@@ -313,10 +310,8 @@
$('#%(id)sShow').attr('class', 'hidden');
}
});''' % {'id': divid})
- showlabel = self._cw._('show filter form')
- hidelabel = self._cw._('hide filter form')
- return [component.Link(showhide, showlabel, id='%sShow' % divid),
- component.Link(showhide, hidelabel, id='%sHide' % divid)]
+ showlabel = self._cw._('toggle filter')
+ return [component.Link(showhide, showlabel, id='%sToggle' % divid)]
class AbstractColumnRenderer(object):
@@ -332,14 +327,14 @@
:attr: `header`, the column header. If None, default to `_(colid)`
:attr: `addcount`, if True, add the table size in parenthezis beside the header
:attr: `trheader`, should the header be translated
- :attr: `escapeheader`, should the header be xml_escape'd
+ :attr: `escapeheader`, should the header be xml_escaped
:attr: `sortable`, tell if the column is sortable
:attr: `view`, the table view
:attr: `_cw`, the request object
:attr: `colid`, the column identifier
:attr: `attributes`, dictionary of attributes to put on the HTML tag when
the cell is rendered
- """
+ """ #'# make emacs
attributes = {}
empty_cell_content = u' '
@@ -576,7 +571,7 @@
renderer.
.. autoclass:: RsetTableColRenderer
- """
+ """ #'# make emacs happier
__regid__ = 'table'
# selector trick for bw compath with the former :class:TableView
__select__ = AnyRsetView.__select__ & (~match_kwargs(
@@ -599,16 +594,19 @@
# may be listed in possible views
return self.__regid__ == 'table'
- def call(self, headers=None, displaycols=None, cellvids=None, **kwargs):
+ def call(self, headers=None, displaycols=None, cellvids=None,
+ paginate=None, **kwargs):
if self.headers:
self.headers = [h and self._cw._(h) for h in self.headers]
- if (headers or displaycols or cellvids):
+ if (headers or displaycols or cellvids or paginate):
if headers is not None:
self.headers = headers
if displaycols is not None:
self.displaycols = displaycols
if cellvids is not None:
self.cellvids = cellvids
+ if paginate is not None:
+ self.paginable = paginate
if kwargs:
# old table view arguments that we can safely ignore thanks to
# selectors
@@ -883,7 +881,7 @@
default_column_renderer_class = EntityTableColRenderer
columns = None # to be defined in concret class
- def call(self, columns=None):
+ def call(self, columns=None, **kwargs):
if columns is not None:
self.columns = columns
self.layout_render(self.w)
--- a/web/views/tabs.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/tabs.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -47,7 +47,15 @@
"""a lazy version of wview"""
w = w or self.w
self._cw.add_js('cubicweb.ajax.js')
+ # the form is copied into urlparams to please the inner views
+ # that might want to take params from it
+ # beware of already present rql or eid elements
+ # to be safe of collision a proper argument passing protocol
+ # (with namespaces) should be used instead of the current
+ # ad-hockery
urlparams = self._cw.form.copy()
+ urlparams.pop('rql', None)
+ urlparams.pop('eid', None)
urlparams.update({'vid' : vid, 'fname' : 'view'})
if rql:
urlparams['rql'] = rql
@@ -82,6 +90,7 @@
class TabsMixin(LazyViewMixin):
"""a tab mixin to easily get jQuery based, lazy, ajax tabs"""
+ lazy = True
@property
def cookie_name(self):
@@ -114,7 +123,7 @@
vid = tabkwargs.get('vid', tabid)
domid = uilib.domid(tabid)
try:
- viewsvreg.select(vid, self._cw, **tabkwargs)
+ viewsvreg.select(vid, self._cw, tabid=domid, **tabkwargs)
except NoSelectableObject:
continue
selected_tabs.append((tabid, domid, tabkwargs))
@@ -149,17 +158,20 @@
w(u'</ul>')
for tabid, domid, tabkwargs in tabs:
w(u'<div id="%s">' % domid)
- tabkwargs.setdefault('tabid', domid)
- tabkwargs.setdefault('vid', tabid)
- tabkwargs.setdefault('rset', self.cw_rset)
- self.lazyview(**tabkwargs)
+ if self.lazy:
+ tabkwargs.setdefault('tabid', domid)
+ tabkwargs.setdefault('vid', tabid)
+ self.lazyview(**tabkwargs)
+ else:
+ self._cw.view(tabid, w=self.w, **tabkwargs)
w(u'</div>')
w(u'</div>')
# call the setTab() JS function *after* each tab is generated
# because the callback binding needs to be done before
# XXX make work history: true
- self._cw.add_onload(u"""
- jQuery('#entity-tabs-%(eeid)s').tabs(
+ if self.lazy:
+ self._cw.add_onload(u"""
+ jQuery('#entity-tabs-%(uid)s').tabs(
{ selected: %(tabindex)s,
select: function(event, ui) {
setTab(ui.panel.id, '%(cookiename)s');
@@ -167,9 +179,13 @@
});
setTab('%(domid)s', '%(cookiename)s');
""" % {'tabindex' : active_tab_idx,
- 'domid' : active_tab,
- 'eeid' : (entity and entity.eid or uid),
+ 'domid' : active_tab,
+ 'uid' : uid,
'cookiename' : self.cookie_name})
+ else:
+ self._cw.add_onload(
+ u"jQuery('#entity-tabs-%(uid)s').tabs({selected: %(tabindex)s});"
+ % {'tabindex': active_tab_idx, 'uid': uid})
class EntityRelationView(EntityView):
@@ -210,8 +226,7 @@
tabs = [_('main_tab')]
default_tab = 'main_tab'
- def cell_call(self, row, col):
- entity = self.cw_rset.complete_entity(row, col)
+ def render_entity(self, entity):
self.render_entity_toolbox(entity)
self.w(u'<div class="tabbedprimary"></div>')
self.render_entity_title(entity)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/undohistory.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,224 @@
+# copyright 2012 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/>.
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+
+from logilab.common.registry import Predicate
+
+from cubicweb import UnknownEid, tags, transaction as tx
+from cubicweb.view import View, StartupView
+from cubicweb.predicates import match_kwargs, ExpectedValuePredicate
+from cubicweb.schema import display_name
+
+
+class undoable_action(Predicate):
+ """Select only undoable actions depending on filters provided. Undo Action
+ is expected to be specified by the `tx_action` argument.
+
+ Currently the only implemented filter is:
+
+ :param action_type: chars among CUDAR (standing for Create, Update, Delete,
+ Add, Remove)
+ """
+
+ # XXX FIXME : this selector should be completed to allow selection on the
+ # entity or relation types and public / private.
+ def __init__(self, action_type='CUDAR'):
+ assert not set(action_type) - set('CUDAR')
+ self.action_type = action_type
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__, ', '.join(
+ "%s=%v" % (str(k), str(v)) for k, v in kwargs.iteritems() ))
+
+ def __call__(self, cls, req, tx_action=None, **kwargs):
+ # tx_action is expected to be a transaction.AbstractAction
+ if not isinstance(tx_action, tx.AbstractAction):
+ return 0
+ # Filter according to action type
+ return int(tx_action.action in self.action_type)
+
+
+class UndoHistoryView(StartupView):
+ __regid__ = 'undohistory'
+ title = _('Undoing')
+ item_vid = 'undoable-transaction-view'
+ cache_max_age = 0
+
+ redirect_path = 'view' #TODO
+ redirect_params = dict(vid='undohistory') #TODO
+ public_actions_only = True
+
+ # TODO Allow to choose if if want all actions or only the public ones
+ # (default)
+
+ def call(self, **kwargs):
+ txs = self._cw.cnx.undoable_transactions()
+ if txs :
+ self.w(u"<ul class='undo-transactions'>")
+ for tx in txs:
+ self.cell_call(tx)
+ self.w(u"</ul>")
+
+ def cell_call(self, tx):
+ self.w(u'<li>')
+ self.wview(self.item_vid, None, txuuid=tx.uuid,
+ public=self.public_actions_only,
+ redirect_path=self.redirect_path,
+ redirect_params=self.redirect_params)
+ self.w(u'</li>\n')
+
+
+class UndoableTransactionView(View):
+ __regid__ = 'undoable-transaction-view'
+ __select__ = View.__select__ & match_kwargs('txuuid')
+
+ item_vid = 'undoable-action-list-view'
+ cache_max_age = 0
+
+ def build_undo_link(self, txuuid,
+ redirect_path=None, redirect_params=None):
+ """ the kwargs are passed to build_url"""
+ _ = self._cw._
+ redirect = {}
+ if redirect_path:
+ redirect['__redirectpath'] = redirect_path
+ if redirect_params:
+ if isinstance(redirect_params, dict):
+ redirect['__redirectparams'] = self._cw.build_url_params(**redirect_params)
+ else:
+ redirect['__redirectparams'] = redirect_params
+ link_url = self._cw.build_url('undo', txuuid=txuuid, **redirect)
+ msg = u"<span class='undo'>%s</span>" % tags.a( _('undo'), href=link_url)
+ return msg
+
+ def call(self, txuuid, public=True,
+ redirect_path=None, redirect_params=None):
+ _ = self._cw._
+ txinfo = self._cw.cnx.transaction_info(txuuid)
+ try:
+ #XXX Under some unknown circumstances txinfo.user_eid=-1
+ user = self._cw.entity_from_eid(txinfo.user_eid)
+ except UnknownEid:
+ user = None
+ undo_url = self.build_undo_link(txuuid,
+ redirect_path=redirect_path,
+ redirect_params=redirect_params)
+ txinfo_dict = dict( dt = self._cw.format_date(txinfo.datetime, time=True),
+ user_eid = txinfo.user_eid,
+ user = user and user.view('outofcontext') or _("undefined user"),
+ txuuid = txuuid,
+ undo_link = undo_url)
+ self.w( _("By %(user)s on %(dt)s [%(undo_link)s]") % txinfo_dict)
+
+ tx_actions = txinfo.actions_list(public=public)
+ if tx_actions :
+ self.wview(self.item_vid, None, tx_actions=tx_actions)
+
+
+class UndoableActionListView(View):
+ __regid__ = 'undoable-action-list-view'
+ __select__ = View.__select__ & match_kwargs('tx_actions')
+ title = _('Undoable actions')
+ item_vid = 'undoable-action-view'
+ cache_max_age = 0
+
+ def call(self, tx_actions):
+ if tx_actions :
+ self.w(u"<ol class='undo-actions'>")
+ for action in tx_actions:
+ self.cell_call(action)
+ self.w(u"</ol>")
+
+ def cell_call(self, action):
+ self.w(u'<li>')
+ self.wview(self.item_vid, None, tx_action=action)
+ self.w(u'</li>\n')
+
+
+class UndoableActionBaseView(View):
+ __regid__ = 'undoable-action-view'
+ __abstract__ = True
+
+ def call(self, tx_action):
+ raise NotImplementedError(self)
+
+ def _build_entity_link(self, eid):
+ try:
+ entity = self._cw.entity_from_eid(eid)
+ return entity.view('outofcontext')
+ except UnknownEid:
+ return _("(suppressed) entity #%d") % eid
+
+ def _build_relation_info(self, rtype, eid_from, eid_to):
+ return dict( rtype=display_name(self._cw, rtype),
+ entity_from=self._build_entity_link(eid_from),
+ entity_to=self._build_entity_link(eid_to) )
+
+ def _build_entity_info(self, etype, eid, changes):
+ return dict( etype=display_name(self._cw, etype),
+ entity=self._build_entity_link(eid),
+ eid=eid,
+ changes=changes)
+
+
+class UndoableAddActionView(UndoableActionBaseView):
+ __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='A')
+
+ def call(self, tx_action):
+ _ = self._cw._
+ self.w(_("Added relation : %(entity_from)s %(rtype)s %(entity_to)s") %
+ self._build_relation_info(tx_action.rtype, tx_action.eid_from, tx_action.eid_to))
+
+
+class UndoableRemoveActionView(UndoableActionBaseView):
+ __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='R')
+
+ def call(self, tx_action):
+ _ = self._cw._
+ self.w(_("Delete relation : %(entity_from)s %(rtype)s %(entity_to)s") %
+ self._build_relation_info(tx_action.rtype, tx_action.eid_from, tx_action.eid_to))
+
+
+class UndoableCreateActionView(UndoableActionBaseView):
+ __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='C')
+
+ def call(self, tx_action):
+ _ = self._cw._
+ self.w(_("Created %(etype)s : %(entity)s") % # : %(changes)s
+ self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes) )
+
+
+class UndoableDeleteActionView(UndoableActionBaseView):
+ __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='D')
+
+ def call(self, tx_action):
+ _ = self._cw._
+ self.w(_("Deleted %(etype)s : %(entity)s") %
+ self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes))
+
+
+class UndoableUpdateActionView(UndoableActionBaseView):
+ __select__ = UndoableActionBaseView.__select__ & undoable_action(action_type='U')
+
+ def call(self, tx_action):
+ _ = self._cw._
+ self.w(_("Updated %(etype)s : %(entity)s") %
+ self._build_entity_info( tx_action.etype, tx_action.eid, tx_action.changes))
--- a/web/views/urlpublishing.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/urlpublishing.py Tue Oct 23 15:00:53 2012 +0200
@@ -106,7 +106,8 @@
:param req: the request object
:type path: str
- :param path: the path of the resource to publish
+ :param path: the path of the resource to publish. If empty, None or "/"
+ "view" is used as the default path.
:rtype: tuple(str, `cubicweb.rset.ResultSet` or None)
:return: the publishing method identifier and an optional result set
--- a/web/views/workflow.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/views/workflow.py Tue Oct 23 15:00:53 2012 +0200
@@ -315,7 +315,7 @@
wf = req.entity_from_eid(wfeid)
rschema = req.vreg.schema[field.name]
param = 'toeid' if field.role == 'subject' else 'fromeid'
- return sorted((e.view('combobox'), e.eid)
+ return sorted((e.view('combobox'), unicode(e.eid))
for e in getattr(wf, 'reverse_%s' % wfrelation)
if rschema.has_perm(req, 'add', **{param: e.eid}))
@@ -330,12 +330,14 @@
def transition_states_vocabulary(form, field):
entity = form.edited_entity
- if not entity.has_eid():
+ if entity.has_eid():
+ wfeid = entity.transition_of[0].eid
+ else:
eids = form.linked_to.get(('transition_of', 'subject'))
if not eids:
return []
- return _wf_items_for_relation(form._cw, eids[0], 'state_of', field)
- return field.relvoc_unrelated(form)
+ wfeid = eids[0]
+ return _wf_items_for_relation(form._cw, wfeid, 'state_of', field)
_afs.tag_subject_of(('*', 'destination_state', '*'), 'main', 'attributes')
_affk.tag_subject_of(('*', 'destination_state', '*'),
@@ -348,12 +350,14 @@
def state_transitions_vocabulary(form, field):
entity = form.edited_entity
- if not entity.has_eid():
+ if entity.has_eid():
+ wfeid = entity.state_of[0].eid
+ else :
eids = form.linked_to.get(('state_of', 'subject'))
- if eids:
- return _wf_items_for_relation(form._cw, eids[0], 'transition_of', field)
- return []
- return field.relvoc_unrelated(form)
+ if not eids:
+ return []
+ wfeid = eids[0]
+ return _wf_items_for_relation(form._cw, wfeid, 'transition_of', field)
_afs.tag_subject_of(('State', 'allowed_transition', '*'), 'main', 'attributes')
_affk.tag_subject_of(('State', 'allowed_transition', '*'),
--- a/web/wdoc/bookmarks_fr.rst Wed Feb 22 11:57:42 2012 +0100
+++ b/web/wdoc/bookmarks_fr.rst Tue Oct 23 15:00:53 2012 +0200
@@ -27,8 +27,4 @@
ayez le droit de les modifier.
-Pour plus de détails sur les relations possibles, veuillez vous réferer au
-schéma_ du composant signet.
-
-.. _`schéma`: eetype/Bookmark?vid=eschema
.. _`préférences utilisateurs`: myprefs
--- a/web/webconfig.py Wed Feb 22 11:57:42 2012 +0100
+++ b/web/webconfig.py Tue Oct 23 15:00:53 2012 +0200
@@ -21,7 +21,7 @@
_ = unicode
import os
-from os.path import join, exists, split
+from os.path import join, exists, split, isdir
from warnings import warn
from logilab.common.decorators import cached
@@ -321,17 +321,19 @@
if not (self.repairing or self.creating):
self.global_set_option('base-url', baseurl)
httpsurl = self['https-url']
- if (self.debugmode or self.mode == 'test'):
- datadir_path = 'data/'
- else:
- datadir_path = 'data/%s/' % self.instance_md5_version()
+ 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 + datadir_path
- self.datadir_url = baseurl + datadir_path
+ self.https_datadir_url = httpsurl + data_relpath
+ self.datadir_url = baseurl + data_relpath
+
+ def data_relpath(self):
+ if self.mode == 'test':
+ return 'data/'
+ return 'data/%s/' % self.instance_md5_version()
def _build_ui_properties(self):
# self.datadir_url[:-1] to remove trailing /
@@ -405,7 +407,8 @@
rdir, filename = split(rpath)
if rdir:
staticdir = join(staticdir, rdir)
- os.makedirs(staticdir)
+ if not isdir(staticdir) and 'w' in mode:
+ os.makedirs(staticdir)
return file(join(staticdir, filename), mode)
def static_file_add(self, rpath, data):
--- a/wsgi/handler.py Wed Feb 22 11:57:42 2012 +0100
+++ b/wsgi/handler.py Tue Oct 23 15:00:53 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,14 +15,16 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""WSGI request handler for cubicweb
+"""WSGI request handler for cubicweb"""
-"""
+
__docformat__ = "restructuredtext en"
+from itertools import chain, repeat, izip
+
from cubicweb import AuthenticationError
-from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
+from cubicweb.web import DirectResponse
from cubicweb.web.application import CubicWebPublisher
from cubicweb.wsgi.request import CubicWebWsgiRequest
@@ -71,7 +73,6 @@
505: 'HTTP VERSION NOT SUPPORTED',
}
-
class WSGIResponse(object):
"""encapsulates the wsgi response parameters
(code, headers and body if there is one)
@@ -79,7 +80,9 @@
def __init__(self, code, req, body=None):
text = STATUS_CODE_TEXT.get(code, 'UNKNOWN STATUS CODE')
self.status = '%s %s' % (code, text)
- self.headers = [(str(k), str(v)) for k, v in req.headers_out.items()]
+ self.headers = list(chain(*[izip(repeat(k), v)
+ for k, v in req.headers_out.getAllRawHeaders()]))
+ self.headers = [(str(k), str(v)) for k, v in self.headers]
if body:
self.body = [body]
else:
@@ -103,95 +106,31 @@
def __init__(self, config, vreg=None):
self.appli = CubicWebPublisher(config, vreg=vreg)
self.config = config
- self.base_url = None
-# self.base_url = config['base-url'] or config.default_base_url()
-# assert self.base_url[-1] == '/'
-# self.https_url = config['https-url']
-# assert not self.https_url or self.https_url[-1] == '/'
+ self.base_url = config['base-url']
+ self.https_url = config['https-url']
self.url_rewriter = self.appli.vreg['components'].select_or_none('urlrewriter')
def _render(self, req):
"""this function performs the actual rendering
- XXX missing: https handling, url rewriting, cache management,
- authentication
"""
if self.base_url is None:
self.base_url = self.config._base_url = req.base_url()
- # XXX https handling needs to be implemented
- if req.authmode == 'http':
- # activate realm-based auth
- realm = self.config['realm']
- req.set_header('WWW-Authenticate', [('Basic', {'realm' : realm })], raw=False)
try:
- self.appli.connect(req)
- except Redirect, ex:
- return self.redirect(req, ex.location)
- path = req.path
- if not path or path == "/":
- path = 'view'
- try:
- result = self.appli.publish(path, req)
+ path = req.path
+ result = self.appli.handle_request(req, path)
except DirectResponse, ex:
- return WSGIResponse(200, req, ex.response)
- except StatusResponse, ex:
- return WSGIResponse(ex.status, req, ex.content)
- except AuthenticationError: # must be before AuthenticationError
- return self.request_auth(req)
- except LogOut:
- if self.config['auth-mode'] == 'cookie':
- # in cookie mode redirecting to the index view is enough :
- # either anonymous connection is allowed and the page will
- # be displayed or we'll be redirected to the login form
- msg = req._('you have been logged out')
-# if req.https:
-# req._base_url = self.base_url
-# req.https = False
- url = req.build_url('view', vid='index', __message=msg)
- return self.redirect(req, url)
- else:
- # in http we have to request auth to flush current http auth
- # information
- return self.request_auth(req, loggedout=True)
- except Redirect, ex:
- return self.redirect(req, ex.location)
- if not result:
- # no result, something went wrong...
- self.error('no data (%s)', req)
- # 500 Internal server error
- return self.redirect(req, req.build_url('error'))
- return WSGIResponse(200, req, result)
+ return ex.response
+ return WSGIResponse(req.status_out, req, result)
def __call__(self, environ, start_response):
"""WSGI protocol entry point"""
- req = CubicWebWsgiRequest(environ, self.appli.vreg, self.base_url)
+ req = CubicWebWsgiRequest(environ, self.appli.vreg)
response = self._render(req)
start_response(response.status, response.headers)
return response.body
- def redirect(self, req, location):
- """convenience function which builds a redirect WSGIResponse"""
- self.debug('redirecting to %s', location)
- req.set_header('location', str(location))
- return WSGIResponse(303, req)
- def request_auth(self, req, loggedout=False):
- """returns the appropriate WSGIResponse to require the user to log in
- """
-# if self.https_url and req.base_url() != self.https_url:
-# return self.redirect(self.https_url + 'login')
- if self.config['auth-mode'] == 'http':
- code = 401 # UNAUTHORIZED
- else:
- code = 403 # FORBIDDEN
- if loggedout:
-# if req.https:
-# req._base_url = self.base_url
-# req.https = False
- content = self.appli.loggedout_content(req)
- else:
- content = self.appli.need_login_content(req)
- return WSGIResponse(code, req, content)
# these are overridden by set_log_methods below
# only defining here to prevent pylint from complaining
--- a/wsgi/request.py Wed Feb 22 11:57:42 2012 +0100
+++ b/wsgi/request.py Tue Oct 23 15:00:53 2012 +0200
@@ -32,7 +32,8 @@
from cubicweb.web.request import CubicWebRequestBase
from cubicweb.wsgi import (pformat, qs2dict, safe_copyfileobj, parse_file_upload,
- normalize_header)
+ normalize_header)
+from cubicweb.web.http_headers import Headers
@@ -40,22 +41,23 @@
"""most of this code COMES FROM DJANO
"""
- def __init__(self, environ, vreg, base_url=None):
+ def __init__(self, environ, vreg):
self.environ = environ
self.path = environ['PATH_INFO']
self.method = environ['REQUEST_METHOD'].upper()
- self._headers = dict([(normalize_header(k[5:]), v) for k, v in self.environ.items()
- if k.startswith('HTTP_')])
+
+ headers_in = dict((normalize_header(k[5:]), v) for k, v in self.environ.items()
+ if k.startswith('HTTP_'))
https = environ.get("HTTPS") in ('yes', 'on', '1')
- self._base_url = base_url or self.instance_uri()
post, files = self.get_posted_data()
- super(CubicWebWsgiRequest, self).__init__(vreg, https, post)
+
+ super(CubicWebWsgiRequest, self).__init__(vreg, https, post,
+ headers= headers_in)
if files is not None:
for key, (name, _, stream) in files.iteritems():
- name = unicode(name, self.encoding)
+ if name is not None:
+ name = unicode(name, self.encoding)
self.form[key] = (name, stream)
- # prepare output headers
- self.headers_out = {}
def __repr__(self):
# Since this is called as part of error handling, we need to be very
@@ -67,9 +69,6 @@
## cubicweb request interface ################################################
- def base_url(self):
- return self._base_url
-
def http_method(self):
"""returns 'POST', 'GET', 'HEAD', etc."""
return self.method
@@ -91,31 +90,6 @@
return path
- def get_header(self, header, default=None):
- """return the value associated with the given input HTTP header,
- raise KeyError if the header is not set
- """
- return self._headers.get(normalize_header(header), default)
-
- def set_header(self, header, value, raw=True):
- """set an output HTTP header"""
- assert raw, "don't know anything about non-raw headers for wsgi requests"
- self.headers_out[header] = value
-
- def add_header(self, header, value):
- """add an output HTTP header"""
- self.headers_out[header] = value
-
- def remove_header(self, header):
- """remove an output HTTP header"""
- self.headers_out.pop(header, None)
-
- def header_if_modified_since(self):
- """If the HTTP header If-modified-since is set, return the equivalent
- mx date time value (GMT), else return None
- """
- return None
-
## wsgi request helpers ###################################################
def instance_uri(self):
@@ -146,6 +120,8 @@
and self.environ['wsgi.url_scheme'] == 'https'
def get_posted_data(self):
+ # The WSGI spec says 'QUERY_STRING' may be absent.
+ post = qs2dict(self.environ.get('QUERY_STRING', ''))
files = None
if self.method == 'POST':
if self.environ.get('CONTENT_TYPE', '').startswith('multipart'):
@@ -153,12 +129,10 @@
for k, v in self.environ.items()
if k.startswith('HTTP_'))
header_dict['Content-Type'] = self.environ.get('CONTENT_TYPE', '')
- post, files = parse_file_upload(header_dict, self.raw_post_data)
+ post_, files = parse_file_upload(header_dict, self.raw_post_data)
+ post.update(post_)
else:
- post = qs2dict(self.raw_post_data)
- else:
- # The WSGI spec says 'QUERY_STRING' may be absent.
- post = qs2dict(self.environ.get('QUERY_STRING', ''))
+ post.update(qs2dict(self.raw_post_data))
return post, files
@property
@@ -176,20 +150,3 @@
postdata = buf.getvalue()
buf.close()
return postdata
-
- def _validate_cache(self):
- """raise a `DirectResponse` exception if a cached page along the way
- exists and is still usable
- """
- # XXX
-# if self.get_header('Cache-Control') in ('max-age=0', 'no-cache'):
-# # Expires header seems to be required by IE7
-# self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
-# return
-# try:
-# http.checkPreconditions(self._twreq, _PreResponse(self))
-# except http.HTTPError, ex:
-# self.info('valid http cache, no actual rendering')
-# raise DirectResponse(ex.response)
- # Expires header seems to be required by IE7
- self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/zmqclient.py Tue Oct 23 15:00:53 2012 +0200
@@ -0,0 +1,58 @@
+# copyright 2003-2012 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/>.
+"""Source to query another RQL repository using pyro"""
+
+__docformat__ = "restructuredtext en"
+_ = unicode
+
+from functools import partial
+import zmq
+
+
+# XXX hack to overpass old zmq limitation that force to have
+# only one context per python process
+try:
+ from cubicweb.server.cwzmq import ctx
+except ImportError:
+ ctx = zmq.Context()
+
+class ZMQRepositoryClient(object):
+ """
+ This class delegate the overall repository stuff to a remote source.
+
+ So calling a method of this repository will results on calling the
+ corresponding method of the remote source repository.
+
+ Any raised exception on the remote source is propagated locally.
+
+ ZMQ is used as the transport layer and cPickle is used to serialize data.
+ """
+
+ def __init__(self, zmq_address):
+ self.socket = ctx.socket(zmq.REQ)
+ self.socket.connect(zmq_address)
+
+ def __zmqcall__(self, name, *args, **kwargs):
+ self.socket.send_pyobj([name, args, kwargs])
+ result = self.socket.recv_pyobj()
+ if isinstance(result, BaseException):
+ raise result
+ return result
+
+ def __getattr__(self, name):
+ return partial(self.__zmqcall__, name)