--- a/.hgtags Tue Jul 10 10:33:19 2012 +0200
+++ b/.hgtags Tue Jul 10 15:07:52 2012 +0200
@@ -250,7 +250,11 @@
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
+783a5df54dc742e63c8a720b1582ff08366733bd cubicweb-version-3.15.1
+fe5e60862b64f1beed2ccdf3a9c96502dfcd811b cubicweb-debian-version-3.15.1-1
--- a/__init__.py Tue Jul 10 10:33:19 2012 +0200
+++ b/__init__.py Tue Jul 10 15:07:52 2012 +0200
@@ -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'):
@@ -55,6 +56,7 @@
# make all exceptions accessible from the package
from cubicweb._exceptions import *
+from logilab.common.registry import ObjectNotFound, NoSelectableObject, RegistryNotFound
# convert eid to the right type, raise ValueError if it's not a valid eid
typed_eid = int
@@ -77,25 +79,24 @@
"Binary objects must use raw strings, not %s" % data.__class__
StringIO.write(self, data)
- def to_file(self, filename):
+ def to_file(self, fobj):
"""write a binary to disk
the writing is performed in a safe way for files stored on
Windows SMB shares
"""
pos = self.tell()
- with open(filename, 'wb') as fobj:
- self.seek(0)
- if sys.platform == 'win32':
- while True:
- # the 16kB chunksize comes from the shutil module
- # in stdlib
- chunk = self.read(16*1024)
- if not chunk:
- break
- fobj.write(chunk)
- else:
- fobj.write(self.read())
+ self.seek(0)
+ if sys.platform == 'win32':
+ while True:
+ # the 16kB chunksize comes from the shutil module
+ # in stdlib
+ chunk = self.read(16*1024)
+ if not chunk:
+ break
+ fobj.write(chunk)
+ else:
+ fobj.write(self.read())
self.seek(pos)
@staticmethod
@@ -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 = {}
--- a/__pkginfo__.py Tue Jul 10 10:33:19 2012 +0200
+++ b/__pkginfo__.py Tue Jul 10 15:07:52 2012 +0200
@@ -22,7 +22,7 @@
modname = distname = "cubicweb"
-numversion = (3, 14, 8)
+numversion = (3, 15, 1)
version = '.'.join(str(num) for num in numversion)
description = "a repository of entities / relations for knowledge management"
@@ -40,11 +40,10 @@
]
__depends__ = {
- 'logilab-common': '>= 0.57.0',
+ 'logilab-common': '>= 0.58.0',
'logilab-mtconverter': '>= 0.8.0',
- 'rql': '>= 0.29.0',
+ 'rql': '>= 0.31.2',
'yams': '>= 0.34.0',
- 'docutils': '>= 0.6',
#gettext # for xgettext, msgcat, etc...
# web dependancies
'simplejson': '>= 2.0.9',
@@ -52,18 +51,20 @@
'Twisted': '',
# XXX graphviz
# server dependencies
- 'logilab-database': '>= 1.8.1',
+ '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
'fyzz': '>= 0.1.0', # for sparql
'vobject': '>= 0.6.0', # for ical view
'rdflib': None, #
+ 'pyzmq': None,
#'Products.FCKeditor':'',
#'SimpleTAL':'>= 4.1.6',
}
--- a/_exceptions.py Tue Jul 10 10:33:19 2012 +0200
+++ b/_exceptions.py Tue Jul 10 15:07:52 2012 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# 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.
@@ -15,10 +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/>.
-"""Exceptions shared by different cubicweb packages.
+"""Exceptions shared by different cubicweb packages."""
-
-"""
__docformat__ = "restructuredtext en"
from yams import ValidationError
@@ -32,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"""
@@ -83,6 +82,7 @@
class UniqueTogetherError(RepositoryError):
"""raised when a unique_together constraint caused an IntegrityError"""
+
# security exceptions #########################################################
class Unauthorized(SecurityError):
@@ -114,32 +114,8 @@
# registry exceptions #########################################################
-class RegistryException(CubicWebException):
- """raised when an unregistered view is called"""
-
-class RegistryNotFound(RegistryException):
- """raised when an unknown registry is requested
-
- this is usually a programming/typo error...
- """
-
-class ObjectNotFound(RegistryException):
- """raised when an unregistered object is requested
-
- this may be a programming/typo or a misconfiguration error
- """
-
-class NoSelectableObject(RegistryException):
- """raised when no appobject is selectable for a given context."""
- def __init__(self, args, kwargs, appobjects):
- self.args = args
- self.kwargs = kwargs
- self.appobjects = appobjects
-
- def __str__(self):
- return ('args: %s, kwargs: %s\ncandidates: %s'
- % (self.args, self.kwargs.keys(), self.appobjects))
-
+# pre 3.15 bw compat
+from logilab.common.registry import RegistryException, ObjectNotFound, NoSelectableObject
class UnknownProperty(RegistryException):
"""property found in database but unknown in registry"""
@@ -154,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):
@@ -161,3 +166,4 @@
# pylint: disable=W0611
from logilab.common.clcommands import BadCommandUsage
+
--- a/appobject.py Tue Jul 10 10:33:19 2012 +0200
+++ b/appobject.py Tue Jul 10 15:07:52 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,281 +35,25 @@
from logging import getLogger
from warnings import warn
-from logilab.common.deprecation import deprecated
+from logilab.common.deprecation import deprecated, class_renamed
from logilab.common.decorators import classproperty
from logilab.common.logging_ext import set_log_methods
+from logilab.common.registry import yes
from cubicweb.cwconfig import CubicWebConfiguration
-
-def class_regid(cls):
- """returns a unique identifier for an appobject class"""
- return cls.__regid__
-
-# helpers for debugging selectors
-TRACED_OIDS = None
-
-def _trace_selector(cls, selector, args, ret):
- # /!\ lltrace decorates pure function or __call__ method, this
- # means argument order may be different
- if isinstance(cls, Selector):
- selname = str(cls)
- vobj = args[0]
- else:
- selname = selector.__name__
- vobj = cls
- if TRACED_OIDS == 'all' or class_regid(vobj) in TRACED_OIDS:
- #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
- print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
-
-def lltrace(selector):
- """use this decorator on your selectors so the becomes traceable with
- :class:`traced_selection`
- """
- # don't wrap selectors if not in development mode
- if CubicWebConfiguration.mode == 'system': # XXX config.debug
- return selector
- def traced(cls, *args, **kwargs):
- ret = selector(cls, *args, **kwargs)
- if TRACED_OIDS is not None:
- _trace_selector(cls, selector, args, ret)
- return ret
- traced.__name__ = selector.__name__
- traced.__doc__ = selector.__doc__
- return traced
-
-class traced_selection(object):
- """
- Typical usage is :
-
- .. sourcecode:: python
-
- >>> from cubicweb.selectors import traced_selection
- >>> with traced_selection():
- ... # some code in which you want to debug selectors
- ... # for all objects
-
- Don't forget the 'from __future__ import with_statement' at the module top-level
- if you're using python prior to 2.6.
-
- This will yield lines like this in the logs::
-
- selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
-
- You can also give to :class:`traced_selection` the identifiers of objects on
- which you want to debug selection ('oid1' and 'oid2' in the example above).
-
- .. sourcecode:: python
-
- >>> with traced_selection( ('regid1', 'regid2') ):
- ... # some code in which you want to debug selectors
- ... # for objects with __regid__ 'regid1' and 'regid2'
-
- A potentially usefull point to set up such a tracing function is
- the `cubicweb.vregistry.Registry.select` method body.
- """
-
- def __init__(self, traced='all'):
- self.traced = traced
-
- def __enter__(self):
- global TRACED_OIDS
- TRACED_OIDS = self.traced
-
- def __exit__(self, exctype, exc, traceback):
- global TRACED_OIDS
- TRACED_OIDS = None
- return traceback is None
-
-# selector base classes and operations ########################################
-
-def objectify_selector(selector_func):
- """Most of the time, a simple score function is enough to build a selector.
- The :func:`objectify_selector` decorator turn it into a proper selector
- class::
-
- @objectify_selector
- def one(cls, req, rset=None, **kwargs):
- return 1
-
- class MyView(View):
- __select__ = View.__select__ & one()
-
- """
- return type(selector_func.__name__, (Selector,),
- {'__doc__': selector_func.__doc__,
- '__call__': lambda self, *a, **kw: selector_func(*a, **kw)})
-
-
-def _instantiate_selector(selector):
- """ensures `selector` is a `Selector` instance
-
- NOTE: This should only be used locally in build___select__()
- XXX: then, why not do it ??
- """
- if isinstance(selector, types.FunctionType):
- return objectify_selector(selector)()
- if isinstance(selector, type) and issubclass(selector, Selector):
- return selector()
- return selector
-
-
-class Selector(object):
- """base class for selector classes providing implementation
- for operators ``&``, ``|`` and ``~``
-
- This class is only here to give access to binary operators, the
- selector logic itself should be implemented in the __call__ method
+# XXX for bw compat
+from logilab.common.registry import objectify_predicate, traced_selection, Predicate
- a selector is called to help choosing the correct object for a
- particular context by returning a score (`int`) telling how well
- the class given as first argument apply to the given context.
-
- 0 score means that the class doesn't apply.
- """
-
- @property
- def func_name(self):
- # backward compatibility
- return self.__class__.__name__
-
- def search_selector(self, selector):
- """search for the given selector, selector instance or tuple of
- selectors in the selectors tree. Return None if not found.
- """
- if self is selector:
- return self
- if (isinstance(selector, type) or isinstance(selector, tuple)) and \
- isinstance(self, selector):
- return self
- return None
-
- def __str__(self):
- return self.__class__.__name__
-
- def __and__(self, other):
- return AndSelector(self, other)
- def __rand__(self, other):
- return AndSelector(other, self)
- def __iand__(self, other):
- return AndSelector(self, other)
- def __or__(self, other):
- return OrSelector(self, other)
- def __ror__(self, other):
- return OrSelector(other, self)
- def __ior__(self, other):
- return OrSelector(self, other)
-
- def __invert__(self):
- return NotSelector(self)
-
- # XXX (function | function) or (function & function) not managed yet
-
- def __call__(self, cls, *args, **kwargs):
- return NotImplementedError("selector %s must implement its logic "
- "in its __call__ method" % self.__class__)
-
- def __repr__(self):
- return u'<Selector %s at %x>' % (self.__class__.__name__, id(self))
-
-
-class MultiSelector(Selector):
- """base class for compound selector classes"""
-
- def __init__(self, *selectors):
- self.selectors = self.merge_selectors(selectors)
-
- def __str__(self):
- return '%s(%s)' % (self.__class__.__name__,
- ','.join(str(s) for s in self.selectors))
-
- @classmethod
- def merge_selectors(cls, selectors):
- """deal with selector instanciation when necessary and merge
- multi-selectors if possible:
+objectify_selector = deprecated('[3.15] objectify_selector has been renamed to objectify_predicates in logilab.common.registry')(objectify_predicate)
+traced_selection = deprecated('[3.15] traced_selection has been moved to logilab.common.registry')(traced_selection)
+Selector = class_renamed(
+ 'Selector', Predicate,
+ '[3.15] Selector has been renamed to Predicate in logilab.common.registry')
- AndSelector(AndSelector(sel1, sel2), AndSelector(sel3, sel4))
- ==> AndSelector(sel1, sel2, sel3, sel4)
- """
- merged_selectors = []
- for selector in selectors:
- try:
- selector = _instantiate_selector(selector)
- except Exception:
- pass
- #assert isinstance(selector, Selector), selector
- if isinstance(selector, cls):
- merged_selectors += selector.selectors
- else:
- merged_selectors.append(selector)
- return merged_selectors
-
- def search_selector(self, selector):
- """search for the given selector or selector instance (or tuple of
- selectors) in the selectors tree. Return None if not found
- """
- for childselector in self.selectors:
- if childselector is selector:
- return childselector
- found = childselector.search_selector(selector)
- if found is not None:
- return found
- # if not found in children, maybe we are looking for self?
- return super(MultiSelector, self).search_selector(selector)
-
-
-class AndSelector(MultiSelector):
- """and-chained selectors (formerly known as chainall)"""
- @lltrace
- def __call__(self, cls, *args, **kwargs):
- score = 0
- for selector in self.selectors:
- partscore = selector(cls, *args, **kwargs)
- if not partscore:
- return 0
- score += partscore
- return score
-
-
-class OrSelector(MultiSelector):
- """or-chained selectors (formerly known as chainfirst)"""
- @lltrace
- def __call__(self, cls, *args, **kwargs):
- for selector in self.selectors:
- partscore = selector(cls, *args, **kwargs)
- if partscore:
- return partscore
- return 0
-
-class NotSelector(Selector):
- """negation selector"""
- def __init__(self, selector):
- self.selector = selector
-
- @lltrace
- def __call__(self, cls, *args, **kwargs):
- score = self.selector(cls, *args, **kwargs)
- return int(not score)
-
- def __str__(self):
- return 'NOT(%s)' % self.selector
-
-
-class yes(Selector):
- """Return the score given as parameter, with a default score of 0.5 so any
- other selector take precedence.
-
- Usually used for appobjects which can be selected whatever the context, or
- also sometimes to add arbitrary points to a score.
-
- Take care, `yes(0)` could be named 'no'...
- """
- def __init__(self, score=0.5):
- self.score = score
-
- def __call__(self, *args, **kwargs):
- return self.score
-
+@deprecated('[3.15] lltrace decorator can now be removed')
+def lltrace(func):
+ return func
# the base class for all appobjects ############################################
@@ -464,3 +208,6 @@
info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
set_log_methods(AppObject, getLogger('cubicweb.appobject'))
+
+# defined here to avoid warning on usage on the AppObject class
+yes = deprecated('[3.15] yes has been moved to logilab.common.registry')(yes)
--- a/cwconfig.py Tue Jul 10 10:33:19 2012 +0200
+++ b/cwconfig.py Tue Jul 10 15:07:52 2012 +0200
@@ -386,14 +386,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,
@@ -828,7 +820,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:
@@ -1160,8 +1152,11 @@
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):
"""return a list of files or directories where the registry will look
--- a/cwctl.py Tue Jul 10 10:33:19 2012 +0200
+++ b/cwctl.py Tue Jul 10 15:07:52 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):
@@ -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 Tue Jul 10 10:33:19 2012 +0200
+++ b/cwvreg.py Tue Jul 10 15:07:52 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,12 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-""".. VRegistry:
+""".. RegistryStore:
-The `VRegistry`
----------------
+The `RegistryStore`
+-------------------
-The `VRegistry` can be seen as a two-level dictionary. It contains
+The `RegistryStore` can be seen as a two-level dictionary. It contains
all dynamically loaded objects (subclasses of :ref:`appobject`) to
build a |cubicweb| application. Basically:
@@ -34,7 +34,7 @@
A *registry* holds a specific kind of application objects. There is
for instance a registry for entity classes, another for views, etc...
-The `VRegistry` has two main responsibilities:
+The `RegistryStore` has two main responsibilities:
- being the access point to all registries
@@ -76,13 +76,13 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here are the registration methods that you can use in the `registration_callback`
-to register your objects to the `VRegistry` instance given as argument (usually
+to register your objects to the `RegistryStore` instance given as argument (usually
named `vreg`):
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_all
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_and_replace
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.unregister
+.. automethod:: cubicweb.cwvreg.CWRegistryStore.register_all
+.. automethod:: cubicweb.cwvreg.CWRegistryStore.register_and_replace
+.. automethod:: cubicweb.cwvreg.CWRegistryStore.register
+.. automethod:: cubicweb.cwvreg.CWRegistryStore.unregister
Examples:
@@ -193,41 +193,44 @@
__docformat__ = "restructuredtext en"
_ = unicode
+import sys
+from os.path import join, dirname, realpath
from warnings import warn
from datetime import datetime, date, time, timedelta
from logilab.common.decorators import cached, clear_cache
from logilab.common.deprecation import deprecated, class_deprecated
from logilab.common.modutils import cleanup_sys_modules
+from logilab.common.registry import (
+ RegistryStore, Registry, classid,
+ ObjectNotFound, NoSelectableObject, RegistryNotFound)
from rql import RQLHelper
from yams.constraints import BASE_CONVERTERS
-from cubicweb import (ETYPE_NAME_MAP, Binary, UnknownProperty, UnknownEid,
- ObjectNotFound, NoSelectableObject, RegistryNotFound,
- CW_EVENT_MANAGER)
-from cubicweb.vregistry import VRegistry, Registry, class_regid, classid
+from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER,
+ Binary, UnknownProperty, UnknownEid)
from cubicweb.rtags import RTAGS
+from cubicweb.predicates import (implements, appobject_selectable,
+ _reset_is_instance_cache)
def clear_rtag_objects():
for rtag in RTAGS:
rtag.clear()
def use_interfaces(obj):
- """return interfaces used by the given object by searching for implements
- selectors
+ """return interfaces required by the given object by searching for
+ `implements` predicate
"""
- from cubicweb.selectors import implements
impl = obj.__select__.search_selector(implements)
if impl:
return sorted(impl.expected_ifaces)
return ()
def require_appobject(obj):
- """return interfaces used by the given object by searching for implements
- selectors
+ """return appobjects required by the given object by searching for
+ `appobject_selectable` predicate
"""
- from cubicweb.selectors import appobject_selectable
impl = obj.__select__.search_selector(appobject_selectable)
if impl:
return (impl.registry, impl.regids)
@@ -253,7 +256,11 @@
key=lambda x: x.cw_propval('order'))
-VRegistry.REGISTRY_FACTORY[None] = CWRegistry
+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):
@@ -261,8 +268,7 @@
def clear_caches(self):
clear_cache(self, 'etype_class')
clear_cache(self, 'parent_classes')
- from cubicweb import selectors
- selectors._reset_is_instance_cache(self.vreg)
+ _reset_is_instance_cache(self.vreg)
def initialization_completed(self):
"""on registration completed, clear etype_class internal cache
@@ -272,7 +278,8 @@
self.clear_caches()
def register(self, obj, **kwargs):
- oid = kwargs.get('oid') or class_regid(obj)
+ 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 '
'schema', obj, oid)
@@ -354,8 +361,6 @@
fetchattrs_list.append(set(etypecls.fetch_attrs))
return reduce(set.intersection, fetchattrs_list)
-VRegistry.REGISTRY_FACTORY['etypes'] = ETypeRegistry
-
class ViewsRegistry(CWRegistry):
@@ -389,8 +394,6 @@
self.exception('error while trying to select %s view for %s',
vid, rset)
-VRegistry.REGISTRY_FACTORY['views'] = ViewsRegistry
-
class ActionsRegistry(CWRegistry):
def poss_visible_objects(self, *args, **kwargs):
@@ -408,8 +411,6 @@
result.setdefault(action.category, []).append(action)
return result
-VRegistry.REGISTRY_FACTORY['actions'] = ActionsRegistry
-
class CtxComponentsRegistry(CWRegistry):
def poss_visible_objects(self, *args, **kwargs):
@@ -445,8 +446,6 @@
component.cw_extra_kwargs['context'] = context
return thisctxcomps
-VRegistry.REGISTRY_FACTORY['ctxcomponents'] = CtxComponentsRegistry
-
class BwCompatCWRegistry(object):
def __init__(self, vreg, oldreg, redirecttoreg):
@@ -462,14 +461,15 @@
def clear(self): pass
def initialization_completed(self): pass
-class CubicWebVRegistry(VRegistry):
+
+class CWRegistryStore(RegistryStore):
"""Central registry for the cubicweb instance, extending the generic
- VRegistry with some cubicweb specific stuff.
+ RegistryStore with some cubicweb specific stuff.
This is one of the central object in cubicweb instance, coupling
dynamically loaded objects with the schema and the configuration objects.
- It specializes the VRegistry by adding some convenience methods to access to
+ It specializes the RegistryStore by adding some convenience methods to access to
stored objects. Currently we have the following registries of objects known
by the web instance (library may use some others additional registries):
@@ -492,11 +492,29 @@
plugged into the application
"""
+ REGISTRY_FACTORY = {None: CWRegistry,
+ 'etypes': ETypeRegistry,
+ 'views': ViewsRegistry,
+ 'actions': ActionsRegistry,
+ 'ctxcomponents': CtxComponentsRegistry,
+ }
+
def __init__(self, config, initlog=True):
if initlog:
# first init log service
config.init_log()
- super(CubicWebVRegistry, self).__init__(config)
+ super(CWRegistryStore, self).__init__(config.debugmode)
+ self.config = config
+ # need to clean sys.path this to avoid import confusion pb (i.e. having
+ # the same module loaded as 'cubicweb.web.views' subpackage and as
+ # views' or 'web.views' subpackage. This is mainly for testing purpose,
+ # we should'nt need this in production environment
+ for webdir in (join(dirname(realpath(__file__)), 'web'),
+ join(dirname(__file__), 'web')):
+ if webdir in sys.path:
+ sys.path.remove(webdir)
+ if CW_SOFTWARE_ROOT in sys.path:
+ sys.path.remove(CW_SOFTWARE_ROOT)
self.schema = None
self.initialized = False
# XXX give force_reload (or refactor [re]loading...)
@@ -515,10 +533,10 @@
return self[regid]
def items(self):
- return [item for item in super(CubicWebVRegistry, self).items()
+ return [item for item in super(CWRegistryStore, self).items()
if not item[0] in ('propertydefs', 'propertyvalues')]
def iteritems(self):
- return (item for item in super(CubicWebVRegistry, self).iteritems()
+ return (item for item in super(CWRegistryStore, self).iteritems()
if not item[0] in ('propertydefs', 'propertyvalues'))
def values(self):
@@ -526,9 +544,23 @@
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(CubicWebVRegistry, self).reset()
+ super(CWRegistryStore, self).reset()
self._needs_iface = {}
self._needs_appobject = {}
# two special registries, propertydefs which care all the property
@@ -541,6 +573,17 @@
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)
@@ -597,7 +640,7 @@
the given `ifaces` interfaces at the end of the registration process.
Extra keyword arguments are given to the
- :meth:`~cubicweb.cwvreg.CubicWebVRegistry.register` function.
+ :meth:`~cubicweb.cwvreg.CWRegistryStore.register` function.
"""
self.register(obj, **kwargs)
if not isinstance(ifaces, (tuple, list)):
@@ -613,7 +656,8 @@
If `clear` is true, all objects with the same identifier will be
previously unregistered.
"""
- super(CubicWebVRegistry, self).register(obj, *args, **kwargs)
+ obj = related_appobject(obj)
+ super(CWRegistryStore, self).register(obj, *args, **kwargs)
# XXX bw compat
ifaces = use_interfaces(obj)
if ifaces:
@@ -630,7 +674,7 @@
def register_objects(self, path):
"""overriden to give cubicweb's extrapath (eg cubes package's __path__)
"""
- super(CubicWebVRegistry, self).register_objects(
+ super(CWRegistryStore, self).register_objects(
path, self.config.extrapath)
def initialization_completed(self):
@@ -685,7 +729,7 @@
self.debug('unregister %s (no %s object in registry %s)',
classid(obj), ' or '.join(regids), regname)
self.unregister(obj)
- super(CubicWebVRegistry, self).initialization_completed()
+ super(CWRegistryStore, self).initialization_completed()
for rtag in RTAGS:
# don't check rtags if we don't want to cleanup_interface_sobjects
rtag.init(self.schema, check=self.config.cleanup_interface_sobjects)
--- a/dbapi.py Tue Jul 10 10:33:19 2012 +0200
+++ b/dbapi.py Tue Jul 10 15:07:52 2012 +0200
@@ -58,9 +58,9 @@
attributes since classes are not designed to be shared among multiple
registries.
"""
- defaultcls = cwvreg.VRegistry.REGISTRY_FACTORY[None]
+ defaultcls = cwvreg.CWRegistryStore.REGISTRY_FACTORY[None]
- etypescls = cwvreg.VRegistry.REGISTRY_FACTORY['etypes']
+ etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
orig_etype_class = etypescls.orig_etype_class = etypescls.etype_class
@monkeypatch(defaultcls)
def etype_class(self, etype):
@@ -75,7 +75,7 @@
return usercls
def multiple_connections_unfix():
- etypescls = cwvreg.VRegistry.REGISTRY_FACTORY['etypes']
+ etypescls = cwvreg.CWRegistryStore.REGISTRY_FACTORY['etypes']
etypescls.etype_class = etypescls.orig_etype_class
@@ -93,14 +93,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(config, vreg=vreg)
else: # method == 'pyro'
# resolve the Pyro object
from logilab.common.pyro_ext import ns_get_proxy, get_proxy
@@ -145,8 +149,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 +187,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)
@@ -192,7 +198,7 @@
elif setvreg:
if mulcnx:
multiple_connections_fix()
- vreg = cwvreg.CubicWebVRegistry(config, initlog=initlog)
+ vreg = cwvreg.CWRegistryStore(config, initlog=initlog)
schema = repo.get_schema()
for oldetype, newetype in ETYPE_NAME_MAP.items():
if oldetype in schema:
@@ -207,7 +213,7 @@
def in_memory_repo(config):
"""Return and in_memory Repository object from a config (or vreg)"""
- if isinstance(config, cwvreg.CubicWebVRegistry):
+ if isinstance(config, cwvreg.CWRegistryStore):
vreg = config
config = None
else:
@@ -280,13 +286,16 @@
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 = {}
+ #: Request language identifier eg: 'en'
+ self.lang = None
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)
@@ -335,6 +344,11 @@
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 ###############################################
def entity_cache(self, eid):
@@ -556,6 +570,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):
--- a/debian/changelog Tue Jul 10 10:33:19 2012 +0200
+++ b/debian/changelog Tue Jul 10 15:07:52 2012 +0200
@@ -1,3 +1,15 @@
+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.8-1) unstable; urgency=low
* new upstream release
--- a/debian/control Tue Jul 10 10:33:19 2012 +0200
+++ b/debian/control Tue Jul 10 15:07:52 2012 +0200
@@ -35,8 +35,9 @@
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.1), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2, python-passlib
+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
CubicWeb is a semantic web application framework.
.
@@ -84,8 +85,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.
.
@@ -99,7 +100,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.57.0), python-yams (>= 0.34.0), python-rql (>= 0.29.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.34.0), python-rql (>= 0.31.2), python-lxml
Recommends: python-simpletal (>= 4.0), python-crypto
Conflicts: cubicweb-core
Replaces: cubicweb-core
--- a/devtools/__init__.py Tue Jul 10 10:33:19 2012 +0200
+++ b/devtools/__init__.py Tue Jul 10 15:07:52 2012 +0200
@@ -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"""
@@ -480,8 +480,8 @@
session = repo._sessions[cnx.sessionid]
session.set_cnxset()
_commit = session.commit
- def keep_cnxset_commit():
- _commit(free_cnxset=False)
+ def keep_cnxset_commit(free_cnxset=False):
+ _commit(free_cnxset=free_cnxset)
session.commit = keep_cnxset_commit
pre_setup_func(session, self.config)
session.commit()
--- a/devtools/devctl.py Tue Jul 10 10:33:19 2012 +0200
+++ b/devtools/devctl.py Tue Jul 10 15:07:52 2012 +0200
@@ -107,7 +107,7 @@
notice that relation definitions description and static vocabulary
should be marked using '_' and extracted using xgettext
"""
- from cubicweb.cwvreg import CubicWebVRegistry
+ from cubicweb.cwvreg import CWRegistryStore
if cubedir:
cube = osp.split(cubedir)[-1]
config = DevConfiguration(cube)
@@ -119,7 +119,7 @@
cube = libconfig = None
cleanup_sys_modules(config)
schema = config.load_schema(remove_unused_rtypes=False)
- vreg = CubicWebVRegistry(config)
+ vreg = CWRegistryStore(config)
# set_schema triggers objects registrations
vreg.set_schema(schema)
w(DEFAULT_POT_HEAD)
@@ -138,13 +138,13 @@
w('\n')
vregdone = set()
if libconfig is not None:
- from cubicweb.cwvreg import CubicWebVRegistry, clear_rtag_objects
+ from cubicweb.cwvreg import CWRegistryStore, clear_rtag_objects
libschema = libconfig.load_schema(remove_unused_rtypes=False)
afs = deepcopy(uicfg.autoform_section)
appearsin_addmenu = deepcopy(uicfg.actionbox_appearsin_addmenu)
clear_rtag_objects()
cleanup_sys_modules(libconfig)
- libvreg = CubicWebVRegistry(libconfig)
+ libvreg = CWRegistryStore(libconfig)
libvreg.set_schema(libschema) # trigger objects registration
libafs = uicfg.autoform_section
libappearsin_addmenu = uicfg.actionbox_appearsin_addmenu
--- a/devtools/fake.py Tue Jul 10 10:33:19 2012 +0200
+++ b/devtools/fake.py Tue Jul 10 15:07:52 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.
@@ -23,7 +23,7 @@
from logilab.database import get_db_helper
from cubicweb.req import RequestSessionBase
-from cubicweb.cwvreg import CubicWebVRegistry
+from cubicweb.cwvreg import CWRegistryStore
from cubicweb.web.request import CubicWebRequestBase
from cubicweb.web.http_headers import Headers
@@ -33,7 +33,9 @@
class FakeConfig(dict, BaseApptestConfiguration):
translations = {}
uiprops = {}
+ https_uiprops = {}
apphome = None
+ debugmode = False
def __init__(self, appid='data', apphome=None, cubes=()):
self.appid = appid
self.apphome = apphome
@@ -43,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
@@ -56,12 +59,12 @@
def __init__(self, *args, **kwargs):
if not (args or 'vreg' in kwargs):
- kwargs['vreg'] = CubicWebVRegistry(FakeConfig(), initlog=False)
+ 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)
@@ -73,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
@@ -89,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
@@ -144,7 +135,7 @@
if vreg is None:
vreg = getattr(self.repo, 'vreg', None)
if vreg is None:
- vreg = CubicWebVRegistry(FakeConfig(), initlog=False)
+ vreg = CWRegistryStore(FakeConfig(), initlog=False)
self.vreg = vreg
self.cnxset = FakeConnectionsSet()
self.user = user or FakeUser()
@@ -179,7 +170,7 @@
self._count = 0
self.schema = schema
self.config = config or FakeConfig()
- self.vreg = vreg or CubicWebVRegistry(self.config, initlog=False)
+ self.vreg = vreg or CWRegistryStore(self.config, initlog=False)
self.vreg.schema = schema
self.sources = []
--- a/devtools/test/data/views.py Tue Jul 10 10:33:19 2012 +0200
+++ b/devtools/test/data/views.py Tue Jul 10 15:07:52 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.
@@ -18,7 +18,7 @@
"""only for unit tests !"""
from cubicweb.view import EntityView
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
HTML_PAGE = u"""<html>
<body>
--- a/devtools/testlib.py Tue Jul 10 10:33:19 2012 +0200
+++ b/devtools/testlib.py Tue Jul 10 15:07:52 2012 +0200
@@ -636,9 +636,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)
@@ -649,11 +649,16 @@
dump = json.dumps
args = [dump(arg) for arg in args]
req = self.request(fname=fname, pageid='123', arg=args)
- ctrl = self.vreg['controllers'].select('json', req)
+ 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"""
@@ -690,6 +695,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
@@ -697,25 +716,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)
@@ -741,13 +759,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)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/3.15.rst Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,96 @@
+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
+-----------
+
+* The base registry implementation has been moved to a new
+ `logilab.common.registry` module (see #1916014). This includes code from :
+
+ * `cubicweb.vreg` (the whole things that was in there)
+ * `cw.appobject` (base selectors and all).
+
+ In the process, some renaming was done:
+
+ * the top level registry is now `RegistryStore` (was `VRegistry`), but that
+ should not impact cubicweb client code ;
+
+ * former selectors functions are now known as "predicate", though you still use
+ predicates to build an object'selector ;
+
+ * for consistency, the `objectify_selector` decoraror has hence be renamed to
+ `objectify_predicate` ;
+
+ * 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
+ `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
+ data-feed (i.e. copy based) source ideas.
+
+* New 'zmqrql' source type, similar to 'pyrorql' but using ømq instead of Pyro.
+
+* 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
+----------------------
+
+A new 'undohistory' view expose the undoable transactions and give access to undo
+some of them.
--- a/doc/book/en/admin/instance-config.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/admin/instance-config.rst Tue Jul 10 15:07:52 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]."
+.. _`Configuring the Web server`:
Configuring the Web server
--------------------------
--- a/doc/book/en/annexes/faq.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/annexes/faq.rst Tue Jul 10 15:07:52 2012 +0200
@@ -194,7 +194,7 @@
"""return an iterator on possible objects in this registry for the given
context
"""
- from cubicweb.selectors import traced_selection
+ from logilab.common.registry import traced_selection
with traced_selection():
for appobjects in self.itervalues():
try:
@@ -217,7 +217,7 @@
def _select_view_and_rset(self, rset):
...
try:
- from cubicweb.selectors import traced_selection
+ from logilab.common.registry import traced_selection
with traced_selection():
view = self._cw.vreg['views'].select(vid, req, rset=rset)
except ObjectNotFound:
--- a/doc/book/en/annexes/rql/language.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/annexes/rql/language.rst Tue Jul 10 15:07:52 2012 +0200
@@ -614,15 +614,18 @@
+--------------------------+----------------------------------------+
| :func:`YEAR(Date)` | return the year of a date or datetime |
+--------------------------+----------------------------------------+
-| :func:`MONTH(Date)` | return the year of a date or datetime |
+| :func:`MONTH(Date)` | return the month of a date or datetime |
+--------------------------+----------------------------------------+
-| :func:`DAY(Date)` | return the year of a date or datetime |
+| :func:`DAY(Date)` | return the day of a date or datetime |
++--------------------------+----------------------------------------+
+| :func:`HOUR(Datetime)` | return the hours of a datetime |
+--------------------------+----------------------------------------+
-| :func:`HOUR(Datetime)` | return the year of a datetime |
+| :func:`MINUTE(Datetime)` | return the minutes of a datetime |
+--------------------------+----------------------------------------+
-| :func:`MINUTE(Datetime)` | return the year of a datetime |
+| :func:`SECOND(Datetime)` | return the seconds of a datetime |
+--------------------------+----------------------------------------+
-| :func:`SECOND(Datetime)` | return the year of a datetime |
+| :func:`WEEKDAY(Date)` | return the day of week of a date or |
+| | datetime. Sunday == 1, Saturday == 7. |
+--------------------------+----------------------------------------+
.. _RQLOtherFunctions:
--- a/doc/book/en/devrepo/datamodel/definition.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devrepo/datamodel/definition.rst Tue Jul 10 15:07:52 2012 +0200
@@ -186,6 +186,9 @@
* `default`: default value of the attribute. In case of date types, the values
which could be used correspond to the RQL keywords `TODAY` and `NOW`.
+* `metadata`: Is also accepted as an argument of the attribute contructor. It is
+ not really an attribute property. see `Metadata`_ for details.
+
Properties for `String` attributes:
* `fulltextindexed`: boolean indicating if the attribute is part of
@@ -567,17 +570,41 @@
In any case, identifiers starting with "CW" or "cw" are reserved for
internal use by the framework.
+ .. _Metadata:
+
+ Some attribute using the name of another attribute as prefix are considered
+ metadata. For example, if an EntityType have both a ``data`` and
+ ``data_format`` attribute, ``data_format`` is view as the ``format`` metadata
+ of ``data``. Later the :meth:`cw_attr_metadata` method will allow you to fetch
+ metadata related to an attribute. There are only three valid metadata names:
+ ``format``, ``encoding`` and ``name``.
+
The name of the Python attribute corresponds to the name of the attribute
or the relation in *CubicWeb* application.
An attribute is defined in the schema as follows::
- attr_name = attr_type(properties)
+ attr_name = AttrType(*properties, metadata={})
+
+where
+
+* `AttrType`: is one of the type listed in EntityType_,
+
+* `properties`: is a list of the attribute needs to satisfy (see `Properties`_
+ for more details),
-where `attr_type` is one of the type listed above and `properties` is
-a list of the attribute needs to satisfy (see `Properties`_
-for more details).
+* `metadata`: is a dictionary of meta attributes related to ``attr_name``.
+ Dictionary keys are the name of the meta attribute. Dictionary values
+ attributes objects (like the content of ``AttrType``). For each entry of the
+ metadata dictionary a ``<attr_name>_<key> = <value>`` attribute is
+ automaticaly added to the EntityType. see `Metadata`_ section for details
+ about valid key.
+
+
+ ---
+
+While building your schema
* it is possible to use the attribute `meta` to flag an entity type as a `meta`
(e.g. used to describe/categorize other entities)
--- a/doc/book/en/devrepo/entityclasses/adapters.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devrepo/entityclasses/adapters.rst Tue Jul 10 15:07:52 2012 +0200
@@ -45,9 +45,9 @@
Adapters came with the notion of service identified by the registry identifier
of an adapters, hence dropping the need for explicit interface and the
- :class:`cubicweb.selectors.implements` selector. You should instead use
- :class:`cubicweb.selectors.is_instance` when you want to select on an entity
- type, or :class:`cubicweb.selectors.adaptable` when you want to select on a
+ :class:`cubicweb.predicates.implements` selector. You should instead use
+ :class:`cubicweb.predicates.is_instance` when you want to select on an entity
+ type, or :class:`cubicweb.predicates.adaptable` when you want to select on a
service.
@@ -79,7 +79,7 @@
.. sourcecode:: python
- from cubicweb.selectors import implements
+ from cubicweb.predicates import implements
from cubicweb.interfaces import ITree
from cubicweb.mixins import ITreeMixIn
@@ -97,7 +97,7 @@
.. sourcecode:: python
- from cubicweb.selectors import adaptable, is_instance
+ from cubicweb.predicates import adaptable, is_instance
from cubicweb.entities.adapters import ITreeAdapter
class MyEntityITreeAdapter(ITreeAdapter):
--- a/doc/book/en/devrepo/repo/hooks.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devrepo/repo/hooks.rst Tue Jul 10 15:07:52 2012 +0200
@@ -26,7 +26,7 @@
.. sourcecode:: python
from cubicweb import ValidationError
- from cubicweb.selectors import is_instance
+ from cubicweb.predicates import is_instance
from cubicweb.server.hook import Hook
class PersonAgeRange(Hook):
@@ -162,6 +162,44 @@
:ref:`adv_tuto_security_propagation`.
+Inter-instance communication
+----------------------------
+
+If your application consists of several instances, you may need some means to
+communicate between them. Cubicweb provides a publish/subscribe mechanism
+using ØMQ_. In order to use it, use
+:meth:`~cubicweb.server.cwzmq.ZMQComm.add_subscription` on the
+`repo.app_instances_bus` object. The `callback` will get the message (as a
+list). A message can be sent by calling
+:meth:`~cubicweb.server.cwzmq.ZMQComm.publish` on `repo.app_instances_bus`.
+The first element of the message is the topic which is used for filtering and
+dispatching messages.
+
+.. _ØMQ: http://www.zeromq.org/
+
+.. sourcecode:: python
+
+ class FooHook(hook.Hook):
+ events = ('server_startup',)
+ __regid__ = 'foo_startup'
+
+ def __call__(self):
+ def callback(msg):
+ self.info('received message: %s', ' '.join(msg))
+ self.repo.app_instances_bus.subscribe('hello', callback)
+
+.. sourcecode:: python
+
+ def do_foo(self):
+ actually_do_foo()
+ self._cw.repo.app_instances_bus.publish(['hello', 'world'])
+
+The `zmq-address-pub` configuration variable contains the address used
+by the instance for sending messages, e.g. `tcp://*:1234`. The
+`zmq-address-sub` variable contains a comma-separated list of addresses
+to listen on, e.g. `tcp://localhost:1234, tcp://192.168.1.1:2345`.
+
+
Hooks writing tips
------------------
--- a/doc/book/en/devrepo/vreg.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devrepo/vreg.rst Tue Jul 10 15:07:52 2012 +0200
@@ -1,5 +1,5 @@
-The VRegistry, selectors and application objects
-================================================
+The Registry, selectors and application objects
+===============================================
This chapter deals with some of the core concepts of the |cubicweb| framework
which make it different from other frameworks (and maybe not easy to
@@ -13,107 +13,108 @@
:ref:`VRegistryIntro` chapter.
.. autodocstring:: cubicweb.cwvreg
-.. autodocstring:: cubicweb.selectors
+.. autodocstring:: cubicweb.predicates
.. automodule:: cubicweb.appobject
-Base selectors
---------------
+Base predicates
+---------------
-Selectors are scoring functions that are called by the registry to tell whenever
-an appobject can be selected in a given context. Selector sets are for instance
-the glue that tie views to the data model. Using them appropriately is an
+Predicates are scoring functions that are called by the registry to tell whenever
+an appobject can be selected in a given context. Predicates may be chained
+together using operators to build a selector. A selector is the glue that tie
+views to the data model or whatever input context. Using them appropriately is an
essential part of the construction of well behaved cubes.
-Of course you may have to write your own set of selectors as your needs grows and
-you get familiar with the framework (see :ref:`CustomSelectors`).
+Of course you may have to write your own set of predicates as your needs grows
+and you get familiar with the framework (see :ref:`CustomPredicates`).
-Here is a description of generic selectors provided by CubicWeb that should suit
+Here is a description of generic predicates provided by CubicWeb that should suit
most of your needs.
-Bare selectors
-~~~~~~~~~~~~~~
-Those selectors are somewhat dumb, which doesn't mean they're not (very) useful.
+Bare predicates
+~~~~~~~~~~~~~~~
+Those predicates are somewhat dumb, which doesn't mean they're not (very) useful.
.. autoclass:: cubicweb.appobject.yes
-.. autoclass:: cubicweb.selectors.match_kwargs
-.. autoclass:: cubicweb.selectors.appobject_selectable
-.. autoclass:: cubicweb.selectors.adaptable
-.. autoclass:: cubicweb.selectors.configuration_values
+.. autoclass:: cubicweb.predicates.match_kwargs
+.. autoclass:: cubicweb.predicates.appobject_selectable
+.. autoclass:: cubicweb.predicates.adaptable
+.. autoclass:: cubicweb.predicates.configuration_values
-Result set selectors
+Result set predicates
~~~~~~~~~~~~~~~~~~~~~
-Those selectors are looking for a result set in the context ('rset' argument or
+Those predicates are looking for a result set in the context ('rset' argument or
the input context) and match or not according to its shape. Some of these
-selectors have different behaviour if a particular cell of the result set is
+predicates have different behaviour if a particular cell of the result set is
specified using 'row' and 'col' arguments of the input context or not.
-.. autoclass:: cubicweb.selectors.none_rset
-.. autoclass:: cubicweb.selectors.any_rset
-.. autoclass:: cubicweb.selectors.nonempty_rset
-.. autoclass:: cubicweb.selectors.empty_rset
-.. autoclass:: cubicweb.selectors.one_line_rset
-.. autoclass:: cubicweb.selectors.multi_lines_rset
-.. autoclass:: cubicweb.selectors.multi_columns_rset
-.. autoclass:: cubicweb.selectors.paginated_rset
-.. autoclass:: cubicweb.selectors.sorted_rset
-.. autoclass:: cubicweb.selectors.one_etype_rset
-.. autoclass:: cubicweb.selectors.multi_etypes_rset
+.. autoclass:: cubicweb.predicates.none_rset
+.. autoclass:: cubicweb.predicates.any_rset
+.. autoclass:: cubicweb.predicates.nonempty_rset
+.. autoclass:: cubicweb.predicates.empty_rset
+.. autoclass:: cubicweb.predicates.one_line_rset
+.. autoclass:: cubicweb.predicates.multi_lines_rset
+.. autoclass:: cubicweb.predicates.multi_columns_rset
+.. autoclass:: cubicweb.predicates.paginated_rset
+.. autoclass:: cubicweb.predicates.sorted_rset
+.. autoclass:: cubicweb.predicates.one_etype_rset
+.. autoclass:: cubicweb.predicates.multi_etypes_rset
-Entity selectors
-~~~~~~~~~~~~~~~~
-Those selectors are looking for either an `entity` argument in the input context,
+Entity predicates
+~~~~~~~~~~~~~~~~~
+Those predicates are looking for either an `entity` argument in the input context,
or entity found in the result set ('rset' argument or the input context) and
match or not according to entity's (instance or class) properties.
-.. autoclass:: cubicweb.selectors.non_final_entity
-.. autoclass:: cubicweb.selectors.is_instance
-.. autoclass:: cubicweb.selectors.score_entity
-.. autoclass:: cubicweb.selectors.rql_condition
-.. autoclass:: cubicweb.selectors.relation_possible
-.. autoclass:: cubicweb.selectors.partial_relation_possible
-.. autoclass:: cubicweb.selectors.has_related_entities
-.. autoclass:: cubicweb.selectors.partial_has_related_entities
-.. autoclass:: cubicweb.selectors.has_permission
-.. autoclass:: cubicweb.selectors.has_add_permission
-.. autoclass:: cubicweb.selectors.has_mimetype
-.. autoclass:: cubicweb.selectors.is_in_state
-.. autofunction:: cubicweb.selectors.on_fire_transition
-.. autoclass:: cubicweb.selectors.implements
+.. autoclass:: cubicweb.predicates.non_final_entity
+.. autoclass:: cubicweb.predicates.is_instance
+.. autoclass:: cubicweb.predicates.score_entity
+.. autoclass:: cubicweb.predicates.rql_condition
+.. autoclass:: cubicweb.predicates.relation_possible
+.. autoclass:: cubicweb.predicates.partial_relation_possible
+.. autoclass:: cubicweb.predicates.has_related_entities
+.. autoclass:: cubicweb.predicates.partial_has_related_entities
+.. autoclass:: cubicweb.predicates.has_permission
+.. autoclass:: cubicweb.predicates.has_add_permission
+.. autoclass:: cubicweb.predicates.has_mimetype
+.. autoclass:: cubicweb.predicates.is_in_state
+.. autofunction:: cubicweb.predicates.on_fire_transition
+.. autoclass:: cubicweb.predicates.implements
-Logged user selectors
-~~~~~~~~~~~~~~~~~~~~~
-Those selectors are looking for properties of the user issuing the request.
+Logged user predicates
+~~~~~~~~~~~~~~~~~~~~~~
+Those predicates are looking for properties of the user issuing the request.
-.. autoclass:: cubicweb.selectors.match_user_groups
+.. autoclass:: cubicweb.predicates.match_user_groups
-Web request selectors
-~~~~~~~~~~~~~~~~~~~~~
-Those selectors are looking for properties of *web* request, they can not be
+Web request predicates
+~~~~~~~~~~~~~~~~~~~~~~
+Those predicates are looking for properties of *web* request, they can not be
used on the data repository side.
-.. autoclass:: cubicweb.selectors.no_cnx
-.. autoclass:: cubicweb.selectors.anonymous_user
-.. autoclass:: cubicweb.selectors.authenticated_user
-.. autoclass:: cubicweb.selectors.match_form_params
-.. autoclass:: cubicweb.selectors.match_search_state
-.. autoclass:: cubicweb.selectors.match_context_prop
-.. autoclass:: cubicweb.selectors.match_context
-.. autoclass:: cubicweb.selectors.match_view
-.. autoclass:: cubicweb.selectors.primary_view
-.. autoclass:: cubicweb.selectors.contextual
-.. autoclass:: cubicweb.selectors.specified_etype_implements
-.. autoclass:: cubicweb.selectors.attribute_edited
-.. autoclass:: cubicweb.selectors.match_transition
+.. autoclass:: cubicweb.predicates.no_cnx
+.. autoclass:: cubicweb.predicates.anonymous_user
+.. autoclass:: cubicweb.predicates.authenticated_user
+.. autoclass:: cubicweb.predicates.match_form_params
+.. autoclass:: cubicweb.predicates.match_search_state
+.. autoclass:: cubicweb.predicates.match_context_prop
+.. autoclass:: cubicweb.predicates.match_context
+.. autoclass:: cubicweb.predicates.match_view
+.. autoclass:: cubicweb.predicates.primary_view
+.. autoclass:: cubicweb.predicates.contextual
+.. autoclass:: cubicweb.predicates.specified_etype_implements
+.. autoclass:: cubicweb.predicates.attribute_edited
+.. autoclass:: cubicweb.predicates.match_transition
-Other selectors
-~~~~~~~~~~~~~~~
-.. autoclass:: cubicweb.selectors.match_exception
-.. autoclass:: cubicweb.selectors.debug_mode
+Other predicates
+~~~~~~~~~~~~~~~~
+.. autoclass:: cubicweb.predicates.match_exception
+.. autoclass:: cubicweb.predicates.debug_mode
-You'll also find some other (very) specific selectors hidden in other modules
-than :mod:`cubicweb.selectors`.
+You'll also find some other (very) specific predicates hidden in other modules
+than :mod:`cubicweb.predicates`.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devweb/ajax.rst Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,12 @@
+.. _ajax:
+
+Ajax
+----
+
+CubicWeb provides a few helpers to facilitate *javascript <-> python* communications.
+
+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.
+
+.. automodule:: cubicweb.web.views.ajaxcontroller
--- a/doc/book/en/devweb/controllers.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devweb/controllers.rst Tue Jul 10 15:07:52 2012 +0200
@@ -22,10 +22,6 @@
:exc:`NoSelectableObject` errors that may bubble up to its entry point, in an
end-user-friendly way (but other programming errors will slip through)
-* the JSon controller (same module) provides services for Ajax calls,
- typically using JSON as a serialization format for input, and
- sometimes using either JSON or XML for output;
-
* the JSonpController is a wrapper around the ``ViewController`` that
provides jsonp_ services. Padding can be specified with the
``callback`` request parameter. Only *jsonexport* / *ejsonexport*
@@ -36,10 +32,6 @@
* the Login/Logout controllers make effective user login or logout
requests
-.. warning::
-
- JsonController will probably be renamed into AjaxController soon since
- it has nothing to do with json per se.
.. _jsonp: http://en.wikipedia.org/wiki/JSONP
@@ -64,6 +56,13 @@
* the MailBugReport controller (web/views/basecontrollers.py) allows
to quickly have a `reportbug` feature in one's application
+* the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`
+ (:mod:`cubicweb.web.views.ajaxcontroller`) provides
+ services for Ajax calls, typically using JSON as a serialization format
+ for input, and sometimes using either JSON or XML for output. See
+ :ref:`ajax` chapter for more information.
+
+
Registration
++++++++++++
--- a/doc/book/en/devweb/edition/form.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devweb/edition/form.rst Tue Jul 10 15:07:52 2012 +0200
@@ -232,7 +232,7 @@
from logilab.common import date
from logilab.mtconverter import xml_escape
from cubicweb.view import View
- from cubicweb.selectors import match_kwargs
+ from cubicweb.predicates import match_kwargs
from cubicweb.web import RequestError, ProcessFormError
from cubicweb.web import formfields as fields, formwidgets as wdgs
from cubicweb.web.views import forms, calendar
--- a/doc/book/en/devweb/index.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devweb/index.rst Tue Jul 10 15:07:52 2012 +0200
@@ -12,6 +12,7 @@
request
views/index
rtags
+ ajax
js
css
edition/index
--- a/doc/book/en/devweb/js.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devweb/js.rst Tue Jul 10 15:07:52 2012 +0200
@@ -72,21 +72,22 @@
A simple example with asyncRemoteExec
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-In the python side, we have to extend the ``BaseController``
-class. The ``@jsonize`` decorator ensures that the return value of the
-method is encoded as JSON data. By construction, the JSonController
-inputs everything in JSON format.
+On the python side, we have to define an
+:class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` object. The
+simplest way to do that is to use the
+:func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator (for more
+details on this, refer to :ref:`ajax`).
.. sourcecode: python
- from cubicweb.web.views.basecontrollers import JSonController, jsonize
+ from cubicweb.web.views.ajaxcontroller import ajaxfunc
- @monkeypatch(JSonController)
- @jsonize
+ # serialize output to json to get it back easily on the javascript side
+ @ajaxfunc(output_type='json')
def js_say_hello(self, name):
return u'hello %s' % name
-In the javascript side, we do the asynchronous call. Notice how it
+On the javascript side, we do the asynchronous call. Notice how it
creates a `deferred` object. Proper treatment of the return value or
error handling has to be done through the addCallback and addErrback
methods.
@@ -131,7 +132,7 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The server side implementation of `reloadComponent` is the
-js_component method of the JSonController.
+:func:`cubicweb.web.views.ajaxcontroller.component` *AjaxFunction* appobject.
The following function implements a two-steps method to delete a
standard bookmark and refresh the UI, while keeping the UI responsive.
@@ -166,7 +167,8 @@
* `url` (mandatory) should be a complete url (typically referencing
- the JSonController, but this is not strictly mandatory)
+ the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`,
+ but this is not strictly mandatory)
* `data` (optional) is a dictionary of values given to the
controller specified through an `url` argument; some keys may have a
@@ -204,25 +206,23 @@
.. sourcecode:: python
- from cubicweb import typed_eid
- from cubicweb.web.views.basecontrollers import JSonController, xhtmlize
+ from cubicweb.web.views.ajaxcontroller import ajaxfunc
- @monkeypatch(JSonController)
- @xhtmlize
+ @ajaxfunc(output_type='xhtml')
def js_frob_status(self, eid, frobname):
- entity = self._cw.entity_from_eid(typed_eid(eid))
+ entity = self._cw.entity_from_eid(eid)
return entity.view('frob', name=frobname)
.. sourcecode:: javascript
- function update_some_div(divid, eid, frobname) {
+ function updateSomeDiv(divid, eid, frobname) {
var params = {fname:'frob_status', eid: eid, frobname:frobname};
jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post');
}
In this example, the url argument is the base json url of a cube
instance (it should contain something like
-`http://myinstance/json?`). The actual JSonController method name is
+`http://myinstance/ajax?`). The actual AjaxController method name is
encoded in the `params` dictionary using the `fname` key.
A more real-life example
@@ -250,7 +250,7 @@
w(u'</div>')
self._cw.add_onload(u"""
jQuery('#lazy-%(vid)s').bind('%(event)s', function() {
- load_now('#lazy-%(vid)s');});"""
+ loadNow('#lazy-%(vid)s');});"""
% {'event': 'load_%s' % vid, 'vid': vid})
This creates a `div` with a specific event associated to it.
@@ -271,7 +271,7 @@
.. sourcecode:: javascript
- function load_now(eltsel) {
+ function loadNow(eltsel) {
var lazydiv = jQuery(eltsel);
lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
}
@@ -306,18 +306,77 @@
"""trigger an event that will force immediate loading of the view
on dom readyness
"""
- self._cw.add_onload("trigger_load('%s');" % vid)
+ self._cw.add_onload("triggerLoad('%s');" % vid)
The browser-side definition follows.
.. sourcecode:: javascript
- function trigger_load(divid) {
+ function triggerLoad(divid) {
jQuery('#lazy-' + divd).trigger('load_' + divid);
}
-.. XXX userCallback / user_callback
+python/ajax dynamic callbacks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+CubicWeb provides a way to dynamically register a function and make it
+callable from the javascript side. The typical use case for this is a
+situation where you have everything at hand to implement an action
+(whether it be performing a RQL query or executing a few python
+statements) that you'd like to defer to a user click in the web
+interface. In other words, generate an HTML ``<a href=...`` link that
+would execute your few lines of code.
+
+The trick is to create a python function and store this function in
+the user's session data. You will then be able to access it later.
+While this might sound hard to implement, it's actually quite easy
+thanks to the ``_cw.user_callback()``. This method takes a function,
+registers it and returns a javascript instruction suitable for
+``href`` or ``onclick`` usage. The call is then performed
+asynchronously.
+
+Here's a simplified example taken from the vcreview_ cube that will
+generate a link to change an entity state directly without the
+standard intermediate *comment / validate* step:
+
+.. sourcecode:: python
+
+ def entity_call(self, entity):
+ # [...]
+ def change_state(req, eid):
+ entity = req.entity_from_eid(eid)
+ entity.cw_adapt_to('IWorkflowable').fire_transition('done')
+ url = self._cw.user_callback(change_state, (entity.eid,))
+ self.w(tags.input(type='button', onclick=url, value=self._cw._('mark as done')))
+
+
+The ``change_state`` callback function is registered with
+``self._cw.user_callback()`` which returns the ``url`` value directly
+used for the ``onclick`` attribute of the button. On the javascript
+side, the ``userCallback()`` function is used but you most probably
+won't have to bother with it.
+
+Of course, when dealing with session data, the question of session
+cleaning pops up immediately. If you use ``user_callback()``, the
+registered function will be deleted automatically at some point
+as any other session data. If you want your function to be deleted once
+the web page is unloaded or when the user has clicked once on your link, then
+``_cw.register_onetime_callback()`` is what you need. It behaves as
+``_cw.user_callback()`` but stores the function in page data instead
+of global session data.
+
+
+.. Warning::
+
+ Be careful when registering functions with closures, keep in mind that
+ enclosed data will be kept in memory until the session gets cleared. Also,
+ if you keep entities or any object referecing the current ``req`` object, you
+ might have problems reusing them later because the underlying session
+ might have been closed at the time the callback gets executed.
+
+
+.. _vcreview: http://www.cubicweb.org/project/cubicweb-vcreview
Javascript library: overview
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -356,12 +415,12 @@
.. toctree::
:maxdepth: 1
-
+
js_api/index
Testing javascript
-~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~
You with the ``cubicweb.qunit.QUnitTestCase`` can include standard Qunit tests
inside the python unittest run . You simply have to define a new class that
--- a/doc/book/en/devweb/property.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devweb/property.rst Tue Jul 10 15:07:52 2012 +0200
@@ -1,3 +1,5 @@
+.. _cwprops:
+
The property mecanism
---------------------
--- a/doc/book/en/devweb/request.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devweb/request.rst Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devweb/resource.rst Tue Jul 10 15:07:52 2012 +0200
@@ -1,3 +1,5 @@
+.. _resources:
+
Locate resources
----------------
--- a/doc/book/en/devweb/views/primary.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devweb/views/primary.rst Tue Jul 10 15:07:52 2012 +0200
@@ -176,7 +176,7 @@
.. sourcecode:: python
- from cubicweb.selectors import is_instance
+ from cubicweb.predicates import is_instance
from cubicweb.web.views.primary import Primaryview
class BlogEntryPrimaryView(PrimaryView):
@@ -206,7 +206,7 @@
.. sourcecode:: python
from logilab.mtconverter import xml_escape
- from cubicweb.selectors import is_instance, one_line_rset
+ from cubicweb.predicates import is_instance, one_line_rset
from cubicweb.web.views.primary import Primaryview
class BlogPrimaryView(PrimaryView):
--- a/doc/book/en/devweb/views/startup.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/devweb/views/startup.rst Tue Jul 10 15:07:52 2012 +0200
@@ -3,7 +3,7 @@
Startup views are views requiring no context, from which you usually start
browsing (for instance the index page). The usual selectors are
-:class:`~cubicweb.selectors.none_rset` or :class:`~cubicweb.selectors.yes`.
+:class:`~cubicweb.predicates.none_rset` or :class:`~logilab.common.registry.yes`.
You'll find here a description of startup views provided by the framework.
--- a/doc/book/en/tutorials/advanced/part02_security.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/tutorials/advanced/part02_security.rst Tue Jul 10 15:07:52 2012 +0200
@@ -187,7 +187,7 @@
.. sourcecode:: python
- from cubicweb.selectors import is_instance
+ from cubicweb.predicates import is_instance
from cubicweb.server import hook
class SetVisibilityOp(hook.DataOperationMixIn, hook.Operation):
--- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst Tue Jul 10 15:07:52 2012 +0200
@@ -23,7 +23,7 @@
.. sourcecode:: python
- from cubicweb.selectors import is_instance
+ from cubicweb.predicates import is_instance
from cubicweb.web import component
from cubicweb.web.views import error
@@ -210,7 +210,7 @@
.. sourcecode:: python
- from cubicweb.selectors import is_instance
+ from cubicweb.predicates import is_instance
from cubicweb.web.views import navigation
--- a/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Tue Jul 10 15:07:52 2012 +0200
@@ -190,7 +190,7 @@
.. sourcecode:: python
- from cubicweb.selectors import none_rset
+ from cubicweb.predicates import none_rset
from cubicweb.web.views import bookmark
from cubes.zone import views as zone
from cubes.tag import views as tag
--- a/doc/book/en/tutorials/base/customizing-the-application.rst Tue Jul 10 10:33:19 2012 +0200
+++ b/doc/book/en/tutorials/base/customizing-the-application.rst Tue Jul 10 15:07:52 2012 +0200
@@ -199,8 +199,8 @@
particular context. When looking for a particular view (e.g. given an
identifier), |cubicweb| computes for each available view with that identifier
a score which is returned by the selector. Then the view with the highest
- score is used. The standard library of selectors is in
- :mod:`cubicweb.selector`.
+ score is used. The standard library of predicates is in
+ :mod:`cubicweb.predicates`.
A view has a set of methods inherited from the :class:`cubicweb.view.View` class,
though you usually don't derive directly from this class but from one of its more
@@ -310,7 +310,7 @@
.. sourcecode:: python
- from cubicweb.selectors import is_instance
+ from cubicweb.predicates import is_instance
from cubicweb.web.views import primary
class CommunityPrimaryView(primary.PrimaryView):
--- a/entities/__init__.py Tue Jul 10 10:33:19 2012 +0200
+++ b/entities/__init__.py Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/entities/adapters.py Tue Jul 10 15:07:52 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.
@@ -29,7 +29,7 @@
from logilab.common.deprecation import class_deprecated
from cubicweb import ValidationError, view
-from cubicweb.selectors import (implements, is_instance, relation_possible,
+from cubicweb.predicates import (implements, is_instance, relation_possible,
match_exception)
from cubicweb.interfaces import IDownloadable, ITree, IProgress, IMileStone
--- a/entities/test/unittest_base.py Tue Jul 10 10:33:19 2012 +0200
+++ b/entities/test/unittest_base.py Tue Jul 10 15:07:52 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,6 +58,12 @@
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):
--- a/entities/wfobjs.py Tue Jul 10 10:33:19 2012 +0200
+++ b/entities/wfobjs.py Tue Jul 10 15:07:52 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,7 @@
from cubicweb.entities import AnyEntity, fetch_config
from cubicweb.view import EntityAdapter
-from cubicweb.selectors import relation_possible
+from cubicweb.predicates import relation_possible
from cubicweb.mixins import MI_REL_TRIGGERS
class WorkflowException(Exception): pass
--- a/entity.py Tue Jul 10 10:33:19 2012 +0200
+++ b/entity.py Tue Jul 10 15:07:52 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 @@
from logilab.common import interface
from logilab.common.decorators import cached
from logilab.common.deprecation import deprecated
+from logilab.common.registry import yes
from logilab.mtconverter import TransformData, TransformError, xml_escape
from rql.utils import rqlvar_maker
@@ -34,7 +35,6 @@
from cubicweb import Unauthorized, typed_eid, neg_role
from cubicweb.utils import support_args
from cubicweb.rset import ResultSet
-from cubicweb.selectors import yes
from cubicweb.appobject import AppObject
from cubicweb.req import _check_cw_unsafe
from cubicweb.schema import (RQLVocabularyConstraint, RQLConstraint,
--- a/etwist/http.py Tue Jul 10 10:33:19 2012 +0200
+++ b/etwist/http.py Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/etwist/request.py Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/etwist/server.py Tue Jul 10 15:07:52 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
@@ -69,175 +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()
-
- def createSimilarFile(self, path):
- # we override this method because twisted calls __init__
- # which we overload with a different signature
- f = self.__class__(self.config, path)
- f.processors = self.processors
- f.indexNames = self.indexNames[:]
- f.childNotFound = self.childNotFound
- return f
-
-
-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)
@@ -249,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
@@ -329,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 Tue Jul 10 10:33:19 2012 +0200
+++ b/etwist/test/unittest_server.py Tue Jul 10 15:07:52 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/hooks/__init__.py Tue Jul 10 10:33:19 2012 +0200
+++ b/hooks/__init__.py Tue Jul 10 15:07:52 2012 +0200
@@ -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/integrity.py Tue Jul 10 10:33:19 2012 +0200
+++ b/hooks/integrity.py Tue Jul 10 15:07:52 2012 +0200
@@ -28,7 +28,7 @@
from cubicweb import ValidationError
from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES,
RQLConstraint, RQLUniqueConstraint)
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.uilib import soup2xhtml
from cubicweb.server import hook
--- a/hooks/metadata.py Tue Jul 10 10:33:19 2012 +0200
+++ b/hooks/metadata.py Tue Jul 10 15:07:52 2012 +0200
@@ -21,7 +21,7 @@
from datetime import datetime
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.server import hook
from cubicweb.server.edition import EditedEntity
@@ -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/notification.py Tue Jul 10 10:33:19 2012 +0200
+++ b/hooks/notification.py Tue Jul 10 15:07:52 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.
@@ -22,7 +22,7 @@
from logilab.common.textutils import normalize_text
from cubicweb import RegistryNotFound
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.server import hook
from cubicweb.sobjects.supervising import SupervisionMailOp
--- a/hooks/security.py Tue Jul 10 10:33:19 2012 +0200
+++ b/hooks/security.py Tue Jul 10 15:07:52 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,8 +21,9 @@
__docformat__ = "restructuredtext en"
+from logilab.common.registry import objectify_predicate
+
from cubicweb import Unauthorized
-from cubicweb.selectors import objectify_selector, lltrace
from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook
@@ -64,8 +65,7 @@
rdef.check_perm(session, action, fromeid=eidfrom, toeid=eidto)
-@objectify_selector
-@lltrace
+@objectify_predicate
def write_security_enabled(cls, req, **kwargs):
if req is None or not req.write_security:
return 0
--- a/hooks/syncschema.py Tue Jul 10 10:33:19 2012 +0200
+++ b/hooks/syncschema.py Tue Jul 10 15:07:52 2012 +0200
@@ -32,7 +32,7 @@
from logilab.common.decorators import clear_cache
from cubicweb import ValidationError
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
CONSTRAINTS, ETYPE_NAME_MAP, display_name)
from cubicweb.server import hook, schemaserial as ss
--- a/hooks/syncsession.py Tue Jul 10 10:33:19 2012 +0200
+++ b/hooks/syncsession.py Tue Jul 10 15:07:52 2012 +0200
@@ -21,7 +21,7 @@
from yams.schema import role_name
from cubicweb import UnknownProperty, ValidationError, BadConnectionId
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.server import hook
--- a/hooks/syncsources.py Tue Jul 10 10:33:19 2012 +0200
+++ b/hooks/syncsources.py Tue Jul 10 15:07:52 2012 +0200
@@ -23,7 +23,7 @@
from yams.schema import role_name
from cubicweb import ValidationError
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.server import SOURCE_TYPES, hook
class SourceHook(hook.Hook):
--- a/hooks/workflow.py Tue Jul 10 10:33:19 2012 +0200
+++ b/hooks/workflow.py Tue Jul 10 15:07:52 2012 +0200
@@ -24,7 +24,7 @@
from yams.schema import role_name
from cubicweb import RepositoryError, ValidationError
-from cubicweb.selectors import is_instance, adaptable
+from cubicweb.predicates import is_instance, adaptable
from cubicweb.server import hook
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/zmq.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# 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 cubicweb.server import hook
+
+class ZMQStopHook(hook.Hook):
+ __regid__ = 'zmqstop'
+ events = ('server_shutdown',)
+
+ def __call__(self):
+ self.repo.app_instances_bus.stop()
+
+class ZMQStartHook(hook.Hook):
+ __regid__ = 'zmqstart'
+ events = ('server_startup',)
+
+ def __call__(self):
+ config = self.repo.config
+ address_pub = config.get('zmq-address-pub')
+ if not address_pub:
+ return
+ from cubicweb.server import cwzmq
+ self.repo.app_instances_bus = cwzmq.ZMQComm()
+ self.repo.app_instances_bus.add_publisher(address_pub)
+ def clear_cache_callback(msg):
+ self.debug('clear_caches: %s', ' '.join(msg))
+ self.repo.clear_caches(msg[1:])
+ self.repo.app_instances_bus.add_subscription('delete', clear_cache_callback)
+ for address in config.get('zmq-address-sub'):
+ 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/i18n/de.po Tue Jul 10 15:07:52 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,13 @@
msgid "About this site"
msgstr "Ãœber diese Seite"
+msgid "Action"
+msgstr ""
+
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
msgid "Any"
msgstr "irgendein"
@@ -390,14 +401,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 +426,10 @@
msgid "Click to sort on this column"
msgstr ""
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
msgid "DEBUG"
msgstr ""
@@ -448,6 +455,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 ""
@@ -759,6 +774,9 @@
msgid "TZTime_plural"
msgstr ""
+msgid "Target"
+msgstr ""
+
#, python-format
msgid "The view %s can not be applied to this query"
msgstr "Die Ansicht %s ist auf diese Anfrage nicht anwendbar."
@@ -872,15 +890,29 @@
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:"
+#, python-format
+msgid "User %(user_eid)s on %(dt)s [%(undo_link)s] \n"
+msgstr ""
+
msgid "Users and groups management"
msgstr ""
@@ -1265,6 +1297,9 @@
msgid "bad value"
msgstr "Unzulässiger Wert"
+msgid "badly formatted url"
+msgstr ""
+
msgid "base url"
msgstr "Basis-URL"
@@ -1349,6 +1384,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 +1424,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 ""
@@ -2030,6 +2084,9 @@
msgid "default"
msgstr "Standardwert"
+msgid "default /base view for (transaction) actions "
+msgstr ""
+
msgid "default text format for rich text fields."
msgstr "Standardformat für Textfelder"
@@ -4241,6 +4298,9 @@
msgid "undo"
msgstr "rückgängig machen"
+msgid "undo last change"
+msgstr ""
+
msgid "unique identifier used to connect to the application"
msgstr "eindeutiger Bezeichner zur Verbindung mit der Anwendung"
@@ -4265,6 +4325,9 @@
msgid "unknown vocabulary:"
msgstr "Unbekanntes Wörterbuch : "
+msgid "unsupported protocol"
+msgstr ""
+
msgid "upassword"
msgstr "Passwort"
@@ -4573,45 +4636,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 Tue Jul 10 10:33:19 2012 +0200
+++ b/i18n/en.po Tue Jul 10 15:07:52 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,13 @@
msgid "About this site"
msgstr ""
+msgid "Action"
+msgstr ""
+
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
msgid "Any"
msgstr ""
@@ -374,12 +385,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 +404,10 @@
msgid "Click to sort on this column"
msgstr ""
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
msgid "DEBUG"
msgstr ""
@@ -424,6 +433,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 ""
@@ -733,6 +750,9 @@
msgid "TZTime_plural"
msgstr "International times"
+msgid "Target"
+msgstr ""
+
#, python-format
msgid "The view %s can not be applied to this query"
msgstr ""
@@ -846,15 +866,29 @@
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 ""
+#, python-format
+msgid "User %(user_eid)s on %(dt)s [%(undo_link)s] \n"
+msgstr ""
+
msgid "Users and groups management"
msgstr ""
@@ -1220,6 +1254,9 @@
msgid "bad value"
msgstr ""
+msgid "badly formatted url"
+msgstr ""
+
msgid "base url"
msgstr ""
@@ -1304,6 +1341,9 @@
msgid "can not resolve entity types:"
msgstr ""
+msgid "can only have one url"
+msgstr ""
+
msgid "can't be changed"
msgstr ""
@@ -1340,6 +1380,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 ""
@@ -1985,6 +2041,9 @@
msgid "default"
msgstr ""
+msgid "default /base view for (transaction) actions "
+msgstr ""
+
msgid "default text format for rich text fields."
msgstr ""
@@ -4139,6 +4198,9 @@
msgid "undo"
msgstr ""
+msgid "undo last change"
+msgstr ""
+
msgid "unique identifier used to connect to the application"
msgstr ""
@@ -4163,6 +4225,9 @@
msgid "unknown vocabulary:"
msgstr ""
+msgid "unsupported protocol"
+msgstr ""
+
msgid "upassword"
msgstr "password"
--- a/i18n/es.po Tue Jul 10 10:33:19 2012 +0200
+++ b/i18n/es.po Tue Jul 10 15:07:52 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,13 @@
msgid "About this site"
msgstr "Información del Sistema"
+msgid "Action"
+msgstr ""
+
+#, python-format
+msgid "Added relation : %(entity_from)s %(rtype)s %(entity_to)s"
+msgstr ""
+
msgid "Any"
msgstr "Cualquiera"
@@ -390,14 +401,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 +426,10 @@
msgid "Click to sort on this column"
msgstr ""
+#, python-format
+msgid "Created %(etype)s : %(entity)s"
+msgstr ""
+
msgid "DEBUG"
msgstr ""
@@ -448,6 +455,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"
@@ -760,6 +775,9 @@
msgid "TZTime_plural"
msgstr "Horas internacionales"
+msgid "Target"
+msgstr ""
+
#, python-format
msgid "The view %s can not be applied to this query"
msgstr "La vista %s no puede ser aplicada a esta búsqueda"
@@ -875,15 +893,29 @@
"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 :"
+#, python-format
+msgid "User %(user_eid)s on %(dt)s [%(undo_link)s] \n"
+msgstr ""
+
msgid "Users and groups management"
msgstr "Usuarios y grupos de administradores"
@@ -1276,6 +1308,9 @@
msgid "bad value"
msgstr "Valor erróneo"
+msgid "badly formatted url"
+msgstr ""
+
msgid "base url"
msgstr "Url de base"
@@ -1360,6 +1395,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 +1434,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 ""
@@ -2059,6 +2113,9 @@
msgid "default"
msgstr "Valor por defecto"
+msgid "default /base view for (transaction) actions "
+msgstr ""
+
msgid "default text format for rich text fields."
msgstr ""
"Formato de texto que se utilizará por defecto para los campos de tipo texto"
@@ -4291,6 +4348,9 @@
msgid "undo"
msgstr "Anular"
+msgid "undo last change"
+msgstr ""
+
msgid "unique identifier used to connect to the application"
msgstr "Identificador único utilizado para conectarse al Sistema"
@@ -4315,6 +4375,9 @@
msgid "unknown vocabulary:"
msgstr "Vocabulario desconocido: "
+msgid "unsupported protocol"
+msgstr ""
+
msgid "upassword"
msgstr "Contraseña"
@@ -4624,54 +4687,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 Tue Jul 10 10:33:19 2012 +0200
+++ b/i18n/fr.po Tue Jul 10 15:07:52 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,13 @@
msgid "About this site"
msgstr "À propos de ce site"
+msgid "Action"
+msgstr "Action"
+
+#, 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"
@@ -390,14 +401,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 +426,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 +455,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"
@@ -760,6 +775,9 @@
msgid "TZTime_plural"
msgstr "Heures internationales"
+msgid "Target"
+msgstr ""
+
#, python-format
msgid "The view %s can not be applied to this query"
msgstr "La vue %s ne peut être appliquée à cette requête"
@@ -875,15 +893,29 @@
"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 :"
+#, python-format
+msgid "By %(user)s on %(dt)s [%(undo_link)s]"
+msgstr "Par %(user)s le %(dt)s [%(undo_link)s] "
+
msgid "Users and groups management"
msgstr "Gestion des utilisateurs et groupes"
@@ -1277,6 +1309,9 @@
msgid "bad value"
msgstr "mauvaise valeur"
+msgid "badly formatted url"
+msgstr ""
+
msgid "base url"
msgstr "url de base"
@@ -1362,6 +1397,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 +1436,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 ""
@@ -2065,6 +2125,9 @@
msgid "default"
msgstr "valeur par défaut"
+msgid "default /base view for (transaction) actions "
+msgstr ""
+
msgid "default text format for rich text fields."
msgstr "format de texte par défaut pour les champs textes"
@@ -4296,6 +4359,9 @@
msgid "undo"
msgstr "annuler"
+msgid "undo last change"
+msgstr "annuler dernier changement"
+
msgid "unique identifier used to connect to the application"
msgstr "identifiant unique utilisé pour se connecter à l'application"
@@ -4320,6 +4386,9 @@
msgid "unknown vocabulary:"
msgstr "vocabulaire inconnu : "
+msgid "unsupported protocol"
+msgstr ""
+
msgid "upassword"
msgstr "mot de passe"
@@ -4627,28 +4696,3 @@
msgstr ""
"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)"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.15.0_Any.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,10 @@
+sync_schema_props_perms('EmailAddress')
+
+for source in rql('CWSource X WHERE X type "ldapuser"').entities():
+ 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.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 Jul 10 15:07:52 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/scripts/ldapuser2ldapfeed.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,76 @@
+"""turn a pyro source into a datafeed source
+
+Once this script is run, execute c-c db-check to cleanup relation tables.
+"""
+import sys
+
+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)
+
+raw_input('Ensure you have shutdown all instances of this application before continuing.'
+ ' Type enter when ready.')
+
+system_source = repo.system_source
+
+from datetime import datetime
+from cubicweb.server.edition import EditedEntity
+
+
+session.mode = 'write' # hold on the connections set
+
+print '******************** backport entity content ***************************'
+
+todelete = {}
+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)
+
+# 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():
+ 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")
+
+
+commit()
--- a/mixins.py Tue Jul 10 10:33:19 2012 +0200
+++ b/mixins.py Tue Jul 10 15:07:52 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,7 +23,7 @@
from logilab.common.decorators import cached
from logilab.common.deprecation import deprecated, class_deprecated
-from cubicweb.selectors import implements
+from cubicweb.predicates import implements
from cubicweb.interfaces import ITree
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/predicates.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,1570 @@
+# 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/>.
+""".. _Selectors:
+
+Predicates and selectors
+------------------------
+
+A predicate is a class testing a particular aspect of a context. A selector is
+built by combining existant predicates or even selectors.
+
+Using and combining existant predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can combine predicates using the `&`, `|` and `~` operators.
+
+When two predicates are combined using the `&` operator, it means that
+both should return a positive score. On success, the sum of scores is
+returned.
+
+When two predicates are combined using the `|` operator, it means that
+one of them should return a positive score. On success, the first
+positive score is returned.
+
+You can also "negate" a predicate by precedeing it by the `~` unary operator.
+
+Of course you can use parenthesis to balance expressions.
+
+Example
+~~~~~~~
+
+The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
+the blog entity itself.
+
+To do that, one defines a method on entity classes that returns the
+RSS stream url for a given entity. The default implementation on
+:class:`~cubicweb.entities.AnyEntity` (the generic entity class used
+as base for all others) and a specific implementation on `Blog` will
+do what we want.
+
+But when we have a result set containing several `Blog` entities (or
+different entities), we don't know on which entity to call the
+aforementioned method. In this case, we keep the generic behaviour.
+
+Hence we have two cases here, one for a single-entity rsets, the other for
+multi-entities rsets.
+
+In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
+
+.. sourcecode:: python
+
+ class RSSIconBox(box.Box):
+ ''' just display the RSS icon on uniform result set '''
+ __select__ = box.Box.__select__ & non_final_entity()
+
+It takes into account:
+
+* the inherited selection criteria (one has to look them up in the class
+ hierarchy to know the details)
+
+* :class:`~cubicweb.predicates.non_final_entity`, which filters on result sets
+ containing non final entities (a 'final entity' being synonym for entity
+ attributes type, eg `String`, `Int`, etc)
+
+This matches our second case. Hence we have to provide a specific component for
+the first case:
+
+.. sourcecode:: python
+
+ class EntityRSSIconBox(RSSIconBox):
+ '''just display the RSS icon on uniform result set for a single entity'''
+ __select__ = RSSIconBox.__select__ & one_line_rset()
+
+Here, one adds the :class:`~cubicweb.predicates.one_line_rset` predicate, which
+filters result sets of size 1. Thus, on a result set containing multiple
+entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
+selectable. However for a result set with one entity, the `EntityRSSIconBox`
+class will have a higher score than `RSSIconBox`, which is what we wanted.
+
+Of course, once this is done, you have to:
+
+* fill in the call method of `EntityRSSIconBox`
+
+* provide the default implementation of the method returning the RSS stream url
+ on :class:`~cubicweb.entities.AnyEntity`
+
+* redefine this method on `Blog`.
+
+
+When to use selectors?
+~~~~~~~~~~~~~~~~~~~~~~
+
+Selectors are to be used whenever arises the need of dispatching on the shape or
+content of a result set or whatever else context (value in request form params,
+authenticated user groups, etc...). That is, almost all the time.
+
+Here is a quick example:
+
+.. sourcecode:: python
+
+ class UserLink(component.Component):
+ '''if the user is the anonymous user, build a link to login else a link
+ to the connected user object with a logout link
+ '''
+ __regid__ = 'loggeduserlink'
+
+ def call(self):
+ if self._cw.session.anonymous_session:
+ # display login link
+ ...
+ else:
+ # display a link to the connected user object with a loggout link
+ ...
+
+The proper way to implement this with |cubicweb| is two have two different
+classes sharing the same identifier but with different selectors so you'll get
+the correct one according to the context.
+
+.. sourcecode:: python
+
+ class UserLink(component.Component):
+ '''display a link to the connected user object with a loggout link'''
+ __regid__ = 'loggeduserlink'
+ __select__ = component.Component.__select__ & authenticated_user()
+
+ def call(self):
+ # display useractions and siteactions
+ ...
+
+ class AnonUserLink(component.Component):
+ '''build a link to login'''
+ __regid__ = 'loggeduserlink'
+ __select__ = component.Component.__select__ & anonymous_user()
+
+ def call(self):
+ # display login link
+ ...
+
+The big advantage, aside readability once you're familiar with the
+system, is that your cube becomes much more easily customizable by
+improving componentization.
+
+
+.. _CustomPredicates:
+
+Defining your own predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. autodocstring:: cubicweb.appobject::objectify_predicate
+
+In other cases, you can take a look at the following abstract base classes:
+
+.. autoclass:: cubicweb.predicates.ExpectedValuePredicate
+.. autoclass:: cubicweb.predicates.EClassPredicate
+.. autoclass:: cubicweb.predicates.EntityPredicate
+
+.. _DebuggingSelectors:
+
+Debugging selection
+~~~~~~~~~~~~~~~~~~~
+
+Once in a while, one needs to understand why a view (or any application object)
+is, or is not selected appropriately. Looking at which predicates fired (or did
+not) is the way. The :class:`logilab.common.registry.traced_selection` context
+manager to help with that, *if you're running your instance in debug mode*.
+
+.. autoclass:: logilab.common.registry.traced_selection
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+import logging
+from warnings import warn
+from operator import eq
+
+from logilab.common.compat import all, any
+from logilab.common.interface import implements as implements_iface
+from logilab.common.registry import Predicate, objectify_predicate
+
+from yams.schema import BASE_TYPES, role_name
+from rql.nodes import Function
+
+from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity,
+ CW_EVENT_MANAGER, role)
+# even if not used, let yes here so it's importable through this module
+from cubicweb.uilib import eid_param
+from cubicweb.schema import split_expression
+
+def score_interface(etypesreg, eclass, iface):
+ """Return XXX if the give object (maybe an instance or class) implements
+ the interface.
+ """
+ if getattr(iface, '__registry__', None) == 'etypes':
+ # adjust score if the interface is an entity class
+ parents, any = etypesreg.parent_classes(eclass.__regid__)
+ if iface is eclass:
+ return len(parents) + 4
+ if iface is any: # Any
+ return 1
+ for index, basecls in enumerate(reversed(parents)):
+ if iface is basecls:
+ return index + 3
+ return 0
+ # XXX iface in implements deprecated in 3.9
+ if implements_iface(eclass, iface):
+ # implementing an interface takes precedence other special Any interface
+ return 2
+ return 0
+
+
+# abstract predicates / mixin helpers ###########################################
+
+class PartialPredicateMixIn(object):
+ """convenience mix-in for predicates that will look into the containing
+ class to find missing information.
+
+ cf. `cubicweb.web.action.LinkToEntityAction` for instance
+ """
+ def __call__(self, cls, *args, **kwargs):
+ self.complete(cls)
+ return super(PartialPredicateMixIn, self).__call__(cls, *args, **kwargs)
+
+
+class EClassPredicate(Predicate):
+ """abstract class for predicates working on *entity class(es)* specified
+ explicitly or found of the result set.
+
+ Here are entity lookup / scoring rules:
+
+ * if `entity` is specified, return score for this entity's class
+
+ * elif `rset`, `select` and `filtered_variable` are specified, return score
+ for the possible classes for variable in the given rql :class:`Select`
+ node
+
+ * elif `rset` and `row` are specified, return score for the class of the
+ entity found in the specified cell, using column specified by `col` or 0
+
+ * elif `rset` is specified return score for each entity class found in the
+ column specified specified by the `col` argument or in column 0 if not
+ specified
+
+ When there are several classes to be evaluated, return the sum of scores for
+ each entity class unless:
+
+ - `mode` == 'all' (the default) and some entity class is scored
+ to 0, in which case 0 is returned
+
+ - `mode` == 'any', in which case the first non-zero score is
+ returned
+
+ - `accept_none` is False and some cell in the column has a None value
+ (this may occurs with outer join)
+ """
+ def __init__(self, once_is_enough=None, accept_none=True, mode='all'):
+ if once_is_enough is not None:
+ warn("[3.14] once_is_enough is deprecated, use mode='any'",
+ DeprecationWarning, stacklevel=2)
+ if once_is_enough:
+ mode = 'any'
+ assert mode in ('any', 'all'), 'bad mode %s' % mode
+ self.once_is_enough = mode == 'any'
+ self.accept_none = accept_none
+
+ def __call__(self, cls, req, rset=None, row=None, col=0, entity=None,
+ select=None, filtered_variable=None,
+ accept_none=None,
+ **kwargs):
+ if entity is not None:
+ return self.score_class(entity.__class__, req)
+ if not rset:
+ return 0
+ if select is not None and filtered_variable is not None:
+ etypes = set(sol[filtered_variable.name] for sol in select.solutions)
+ elif row is None:
+ if accept_none is None:
+ accept_none = self.accept_none
+ if not accept_none and \
+ any(rset[i][col] is None for i in xrange(len(rset))):
+ return 0
+ etypes = rset.column_types(col)
+ else:
+ etype = rset.description[row][col]
+ # may have None in rset.description on outer join
+ if etype is None or rset.rows[row][col] is None:
+ return 0
+ etypes = (etype,)
+ score = 0
+ for etype in etypes:
+ escore = self.score(cls, req, etype)
+ if not escore and not self.once_is_enough:
+ return 0
+ elif self.once_is_enough:
+ return escore
+ score += escore
+ return score
+
+ def score(self, cls, req, etype):
+ if etype in BASE_TYPES:
+ return 0
+ return self.score_class(req.vreg['etypes'].etype_class(etype), req)
+
+ def score_class(self, eclass, req):
+ raise NotImplementedError()
+
+
+class EntityPredicate(EClassPredicate):
+ """abstract class for predicates working on *entity instance(s)* specified
+ explicitly or found of the result set.
+
+ Here are entity lookup / scoring rules:
+
+ * if `entity` is specified, return score for this entity
+
+ * elif `row` is specified, return score for the entity found in the
+ specified cell, using column specified by `col` or 0
+
+ * else return the sum of scores for each entity found in the column
+ specified specified by the `col` argument or in column 0 if not specified,
+ unless:
+
+ - `mode` == 'all' (the default) and some entity class is scored
+ to 0, in which case 0 is returned
+
+ - `mode` == 'any', in which case the first non-zero score is
+ returned
+
+ - `accept_none` is False and some cell in the column has a None value
+ (this may occurs with outer join)
+
+ .. Note::
+ using :class:`EntityPredicate` or :class:`EClassPredicate` as base predicate
+ class impacts performance, since when no entity or row is specified the
+ later works on every different *entity class* found in the result set,
+ while the former works on each *entity* (eg each row of the result set),
+ which may be much more costly.
+ """
+
+ def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
+ **kwargs):
+ if not rset and not kwargs.get('entity'):
+ return 0
+ score = 0
+ if kwargs.get('entity'):
+ score = self.score_entity(kwargs['entity'])
+ elif row is None:
+ col = col or 0
+ if accept_none is None:
+ accept_none = self.accept_none
+ for row, rowvalue in enumerate(rset.rows):
+ if rowvalue[col] is None: # outer join
+ if not accept_none:
+ return 0
+ continue
+ escore = self.score(req, rset, row, col)
+ if not escore and not self.once_is_enough:
+ return 0
+ elif self.once_is_enough:
+ return escore
+ score += escore
+ else:
+ col = col or 0
+ etype = rset.description[row][col]
+ if etype is not None: # outer join
+ score = self.score(req, rset, row, col)
+ return score
+
+ def score(self, req, rset, row, col):
+ try:
+ return self.score_entity(rset.get_entity(row, col))
+ except NotAnEntity:
+ return 0
+
+ def score_entity(self, entity):
+ raise NotImplementedError()
+
+
+class ExpectedValuePredicate(Predicate):
+ """Take a list of expected values as initializer argument and store them
+ into the :attr:`expected` set attribute. You may also give a set as single
+ argument, which will then be referenced as set of expected values,
+ allowing modifications to the given set to be considered.
+
+ You should implement one of :meth:`_values_set(cls, req, **kwargs)` or
+ :meth:`_get_value(cls, req, **kwargs)` method which should respectively
+ return the set of values or the unique possible value for the given context.
+
+ You may also specify a `mode` behaviour as argument, as explained below.
+
+ Returned score is:
+
+ - 0 if `mode` == 'all' (the default) and at least one expected
+ values isn't found
+
+ - 0 if `mode` == 'any' and no expected values isn't found at all
+
+ - else the number of matching values
+
+ Notice `mode` = 'any' with a single expected value has no effect at all.
+ """
+ def __init__(self, *expected, **kwargs):
+ assert expected, self
+ if len(expected) == 1 and isinstance(expected[0], set):
+ self.expected = expected[0]
+ else:
+ self.expected = frozenset(expected)
+ mode = kwargs.pop('mode', 'all')
+ assert mode in ('any', 'all'), 'bad mode %s' % mode
+ self.once_is_enough = mode == 'any'
+ assert not kwargs, 'unexpected arguments %s' % kwargs
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(sorted(str(s) for s in self.expected)))
+
+ def __call__(self, cls, req, **kwargs):
+ values = self._values_set(cls, req, **kwargs)
+ matching = len(values & self.expected)
+ if self.once_is_enough:
+ return matching
+ if matching == len(self.expected):
+ return matching
+ return 0
+
+ def _values_set(self, cls, req, **kwargs):
+ return frozenset( (self._get_value(cls, req, **kwargs),) )
+
+ def _get_value(self, cls, req, **kwargs):
+ raise NotImplementedError()
+
+
+# bare predicates ##############################################################
+
+class match_kwargs(ExpectedValuePredicate):
+ """Return non-zero score if parameter names specified as initializer
+ arguments are specified in the input context.
+
+
+ Return a score corresponding to the number of expected parameters.
+
+ When multiple parameters are expected, all of them should be found in
+ the input context unless `mode` keyword argument is given to 'any',
+ in which case a single matching parameter is enough.
+ """
+
+ def _values_set(self, cls, req, **kwargs):
+ return frozenset(kwargs)
+
+
+class appobject_selectable(Predicate):
+ """Return 1 if another appobject is selectable using the same input context.
+
+ Initializer arguments:
+
+ * `registry`, a registry name
+
+ * `regids`, object identifiers in this registry, one of them should be
+ selectable.
+ """
+ selectable_score = 1
+ def __init__(self, registry, *regids):
+ self.registry = registry
+ self.regids = regids
+
+ def __call__(self, cls, req, **kwargs):
+ for regid in self.regids:
+ try:
+ req.vreg[self.registry].select(regid, req, **kwargs)
+ return self.selectable_score
+ except NoSelectableObject:
+ continue
+ return 0
+
+
+class adaptable(appobject_selectable):
+ """Return 1 if another appobject is selectable using the same input context.
+
+ Initializer arguments:
+
+ * `regids`, adapter identifiers (e.g. interface names) to which the context
+ (usually entities) should be adaptable. One of them should be selectable
+ when multiple identifiers are given.
+ """
+ def __init__(self, *regids):
+ super(adaptable, self).__init__('adapters', *regids)
+
+ def __call__(self, cls, req, **kwargs):
+ kwargs.setdefault('accept_none', False)
+ # being adaptable to an interface should takes precedence other is_instance('Any'),
+ # but not other explicit is_instance('SomeEntityType'), and:
+ # * is_instance('Any') score is 1
+ # * is_instance('SomeEntityType') score is at least 2
+ score = super(adaptable, self).__call__(cls, req, **kwargs)
+ if score >= 2:
+ return score - 0.5
+ if score == 1:
+ return score + 0.5
+ return score
+
+
+class configuration_values(Predicate):
+ """Return 1 if the instance has an option set to a given value(s) in its
+ configuration file.
+ """
+ # XXX this predicate could be evaluated on startup
+ def __init__(self, key, values):
+ self._key = key
+ if not isinstance(values, (tuple, list)):
+ values = (values,)
+ self._values = frozenset(values)
+
+ def __call__(self, cls, req, **kwargs):
+ try:
+ return self._score
+ except AttributeError:
+ if req is None:
+ config = kwargs['repo'].config
+ else:
+ config = req.vreg.config
+ self._score = config[self._key] in self._values
+ return self._score
+
+
+# rset predicates ##############################################################
+
+@objectify_predicate
+def none_rset(cls, req, rset=None, **kwargs):
+ """Return 1 if the result set is None (eg usually not specified)."""
+ if rset is None:
+ return 1
+ return 0
+
+
+# XXX == ~ none_rset
+@objectify_predicate
+def any_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for any result set, whatever the number of rows in it, even 0."""
+ if rset is not None:
+ return 1
+ return 0
+
+
+@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:
+ return 1
+ return 0
+
+
+# XXX == ~ nonempty_rset
+@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:
+ return 1
+ return 0
+
+
+# XXX == multi_lines_rset(1)
+@objectify_predicate
+def one_line_rset(cls, req, rset=None, row=None, **kwargs):
+ """Return 1 if the result set is of size 1, or greater but a specific row in
+ the result set is specified ('row' argument).
+ """
+ if rset is None and 'entity' in kwargs:
+ return 1
+ if rset is not None and (row is not None or rset.rowcount == 1):
+ return 1
+ return 0
+
+
+class multi_lines_rset(Predicate):
+ """Return 1 if the operator expression matches between `num` elements
+ in the result set and the `expected` value if defined.
+
+ By default, multi_lines_rset(expected) matches equality expression:
+ `nb` row(s) in result set equals to expected value
+ But, you can perform richer comparisons by overriding default operator:
+ multi_lines_rset(expected, operator.gt)
+
+ If `expected` is None, return 1 if the result set contains *at least*
+ two rows.
+ If rset is None, return 0.
+ """
+ def __init__(self, expected=None, operator=eq):
+ self.expected = expected
+ self.operator = operator
+
+ def match_expected(self, num):
+ if self.expected is None:
+ return num > 1
+ 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))
+
+
+class multi_columns_rset(multi_lines_rset):
+ """If `nb` is specified, return 1 if the result set has exactly `nb` column
+ per row. Else (`nb` is None), return 1 if the result set contains *at least*
+ two columns per row. Return 0 for empty result set.
+ """
+
+ def __call__(self, cls, req, rset=None, **kwargs):
+ # '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):
+ """Return 1 or more for result set with more rows than one or more page
+ size. You can specify expected number of pages to the initializer (default
+ to one), and you'll get that number of pages as score if the result set is
+ big enough.
+
+ Page size is searched in (respecting order):
+ * a `page_size` argument
+ * a `page_size` form parameters
+ * the `navigation.page-size` property (see :ref:`PersistentProperties`)
+ """
+ def __init__(self, nbpages=1):
+ assert nbpages > 0
+ self.nbpages = nbpages
+
+ def __call__(self, cls, req, rset=None, **kwargs):
+ if rset is None:
+ return 0
+ page_size = kwargs.get('page_size')
+ if page_size is None:
+ page_size = req.form.get('page_size')
+ if page_size is None:
+ page_size = req.property_value('navigation.page-size')
+ else:
+ page_size = int(page_size)
+ if rset.rowcount <= (page_size*self.nbpages):
+ return 0
+ return self.nbpages
+
+
+@objectify_predicate
+def sorted_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for sorted result set (e.g. from an RQL query containing an
+ ORDERBY clause), with exception that it will return 0 if the rset is
+ 'ORDERBY FTIRANK(VAR)' (eg sorted by rank value of the has_text index).
+ """
+ if rset is None:
+ return 0
+ selects = rset.syntax_tree().children
+ if (len(selects) > 1 or
+ not selects[0].orderby or
+ (isinstance(selects[0].orderby[0].term, Function) and
+ selects[0].orderby[0].term.name == 'FTIRANK')
+ ):
+ return 0
+ return 2
+
+
+# XXX == multi_etypes_rset(1)
+@objectify_predicate
+def one_etype_rset(cls, req, rset=None, col=0, **kwargs):
+ """Return 1 if the result set contains entities which are all of the same
+ type in the column specified by the `col` argument of the input context, or
+ in column 0.
+ """
+ if rset is None:
+ return 0
+ if len(rset.column_types(col)) != 1:
+ return 0
+ return 1
+
+
+class multi_etypes_rset(multi_lines_rset):
+ """If `nb` is specified, return 1 if the result set contains `nb` different
+ types of entities in the column specified by the `col` argument of the input
+ context, or in column 0. If `nb` is None, return 1 if the result set contains
+ *at least* two different types of entities.
+ """
+
+ def __call__(self, cls, req, rset=None, col=0, **kwargs):
+ # 'or 0' since we *must not* return None
+ return rset and self.match_expected(len(rset.column_types(col))) or 0
+
+
+@objectify_predicate
+def logged_user_in_rset(cls, req, rset=None, row=None, col=0, **kwargs):
+ """Return positive score if the result set at the specified row / col
+ contains the eid of the logged user.
+ """
+ if rset is None:
+ return 0
+ return req.user.eid == rset[row or 0][col]
+
+
+# entity predicates #############################################################
+
+class non_final_entity(EClassPredicate):
+ """Return 1 for entity of a non final entity type(s). Remember, "final"
+ entity types are String, Int, etc... This is equivalent to
+ `is_instance('Any')` but more optimized.
+
+ See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
+ class lookup / score rules according to the input context.
+ """
+ def score(self, cls, req, etype):
+ if etype in BASE_TYPES:
+ return 0
+ return 1
+
+ def score_class(self, eclass, req):
+ return 1 # necessarily true if we're there
+
+
+class implements(EClassPredicate):
+ """Return non-zero score for entity that are of the given type(s) or
+ implements at least one of the given interface(s). If multiple arguments are
+ given, matching one of them is enough.
+
+ Entity types should be given as string, the corresponding class will be
+ fetched from the entity types registry at selection time.
+
+ 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:: deprecated in cubicweb >= 3.9, use either
+ :class:`~cubicweb.predicates.is_instance` or
+ :class:`~cubicweb.predicates.adaptable`.
+ """
+
+ def __init__(self, *expected_ifaces, **kwargs):
+ emit_warn = kwargs.pop('warn', True)
+ super(implements, self).__init__(**kwargs)
+ self.expected_ifaces = expected_ifaces
+ if emit_warn:
+ warn('[3.9] implements predicate is deprecated, use either '
+ 'is_instance or adaptable', DeprecationWarning, stacklevel=2)
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(str(s) for s in self.expected_ifaces))
+
+ def score_class(self, eclass, req):
+ score = 0
+ etypesreg = req.vreg['etypes']
+ for iface in self.expected_ifaces:
+ if isinstance(iface, basestring):
+ # entity type
+ try:
+ iface = etypesreg.etype_class(iface)
+ except KeyError:
+ continue # entity type not in the schema
+ score += score_interface(etypesreg, eclass, iface)
+ return score
+
+def _reset_is_instance_cache(vreg):
+ vreg._is_instance_predicate_cache = {}
+
+CW_EVENT_MANAGER.bind('before-registry-reset', _reset_is_instance_cache)
+
+class is_instance(EClassPredicate):
+ """Return non-zero score for entity that is an instance of the one of given
+ type(s). If multiple arguments are given, matching one of them is enough.
+
+ Entity types should be given as string, the corresponding class will be
+ fetched from the registry at selection time.
+
+ See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
+ class lookup / score rules according to the input context.
+
+ .. note:: the score will reflect class proximity so the most specific object
+ will be selected.
+ """
+
+ def __init__(self, *expected_etypes, **kwargs):
+ super(is_instance, self).__init__(**kwargs)
+ self.expected_etypes = expected_etypes
+ for etype in self.expected_etypes:
+ assert isinstance(etype, basestring), etype
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(str(s) for s in self.expected_etypes))
+
+ def score_class(self, eclass, req):
+ # cache on vreg to avoid reloading issues
+ try:
+ cache = req.vreg._is_instance_predicate_cache
+ except AttributeError:
+ # XXX 'before-registry-reset' not called for db-api connections
+ cache = req.vreg._is_instance_predicate_cache = {}
+ try:
+ expected_eclasses = cache[self]
+ except KeyError:
+ # turn list of entity types as string into a list of
+ # (entity class, parent classes)
+ etypesreg = req.vreg['etypes']
+ expected_eclasses = cache[self] = []
+ for etype in self.expected_etypes:
+ try:
+ expected_eclasses.append(etypesreg.etype_class(etype))
+ except KeyError:
+ continue # entity type not in the schema
+ parents, any = req.vreg['etypes'].parent_classes(eclass.__regid__)
+ score = 0
+ for expectedcls in expected_eclasses:
+ # adjust score according to class proximity
+ if expectedcls is eclass:
+ score += len(parents) + 4
+ elif expectedcls is any: # Any
+ score += 1
+ else:
+ for index, basecls in enumerate(reversed(parents)):
+ if expectedcls is basecls:
+ score += index + 3
+ break
+ return score
+
+
+class score_entity(EntityPredicate):
+ """Return score according to an arbitrary function given as argument which
+ will be called with input content entity as argument.
+
+ This is a very useful predicate that will usually interest you since it
+ allows a lot of things without having to write a specific predicate.
+
+ The function can return arbitrary value which will be casted to an integer
+ value at the end.
+
+ See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
+ lookup / score rules according to the input context.
+ """
+ def __init__(self, scorefunc, once_is_enough=None, mode='all'):
+ super(score_entity, self).__init__(mode=mode, once_is_enough=once_is_enough)
+ def intscore(*args, **kwargs):
+ score = scorefunc(*args, **kwargs)
+ if not score:
+ return 0
+ if isinstance(score, (int, long)):
+ return score
+ return 1
+ self.score_entity = intscore
+
+
+class has_mimetype(EntityPredicate):
+ """Return 1 if the entity adapt to IDownloadable and has the given MIME type.
+
+ You can give 'image/' to match any image for instance, or 'image/png' to match
+ only PNG images.
+ """
+ def __init__(self, mimetype, once_is_enough=None, mode='all'):
+ super(has_mimetype, self).__init__(mode=mode, once_is_enough=once_is_enough)
+ self.mimetype = mimetype
+
+ def score_entity(self, entity):
+ idownloadable = entity.cw_adapt_to('IDownloadable')
+ if idownloadable is None:
+ return 0
+ mt = idownloadable.download_content_type()
+ if not (mt and mt.startswith(self.mimetype)):
+ return 0
+ return 1
+
+
+class relation_possible(EntityPredicate):
+ """Return 1 for entity that supports the relation, provided that the
+ request's user may do some `action` on it (see below).
+
+ The relation is specified by the following initializer arguments:
+
+ * `rtype`, the name of the relation
+
+ * `role`, the role of the entity in the relation, either 'subject' or
+ 'object', default to 'subject'
+
+ * `target_etype`, optional name of an entity type that should be supported
+ at the other end of the relation
+
+ * `action`, a relation schema action (e.g. one of 'read', 'add', 'delete',
+ default to 'read') which must be granted to the user, else a 0 score will
+ be returned. Give None if you don't want any permission checking.
+
+ * `strict`, boolean (default to False) telling what to do when the user has
+ not globally the permission for the action (eg the action is not granted
+ to one of the user's groups)
+
+ - when strict is False, if there are some local role defined for this
+ action (e.g. using rql expressions), then the permission will be
+ considered as granted
+
+ - when strict is True, then the permission will be actually checked for
+ each entity
+
+ Setting `strict` to True impacts performance for large result set since
+ you'll then get the :class:`~cubicweb.predicates.EntityPredicate` behaviour
+ while otherwise you get the :class:`~cubicweb.predicates.EClassPredicate`'s
+ one. See those classes documentation for entity lookup / score rules
+ according to the input context.
+ """
+
+ def __init__(self, rtype, role='subject', target_etype=None,
+ action='read', strict=False, **kwargs):
+ super(relation_possible, self).__init__(**kwargs)
+ self.rtype = rtype
+ self.role = role
+ self.target_etype = target_etype
+ self.action = action
+ self.strict = strict
+
+ # hack hack hack
+ def __call__(self, cls, req, **kwargs):
+ # hack hack hack
+ if self.strict:
+ return EntityPredicate.__call__(self, cls, req, **kwargs)
+ return EClassPredicate.__call__(self, cls, req, **kwargs)
+
+ def score(self, *args):
+ if self.strict:
+ return EntityPredicate.score(self, *args)
+ return EClassPredicate.score(self, *args)
+
+ def _get_rschema(self, eclass):
+ eschema = eclass.e_schema
+ try:
+ if self.role == 'object':
+ return eschema.objrels[self.rtype]
+ else:
+ return eschema.subjrels[self.rtype]
+ except KeyError:
+ return None
+
+ def score_class(self, eclass, req):
+ rschema = self._get_rschema(eclass)
+ if rschema is None:
+ return 0 # relation not supported
+ eschema = eclass.e_schema
+ if self.target_etype is not None:
+ try:
+ rdef = rschema.role_rdef(eschema, self.target_etype, self.role)
+ except KeyError:
+ return 0
+ if self.action and not rdef.may_have_permission(self.action, req):
+ return 0
+ teschema = req.vreg.schema.eschema(self.target_etype)
+ if not teschema.may_have_permission('read', req):
+ return 0
+ elif self.action:
+ return rschema.may_have_permission(self.action, req, eschema, self.role)
+ return 1
+
+ def score_entity(self, entity):
+ rschema = self._get_rschema(entity)
+ if rschema is None:
+ return 0 # relation not supported
+ if self.action:
+ if self.target_etype is not None:
+ rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role)
+ if self.role == 'subject':
+ if not rschema.has_perm(entity._cw, self.action, fromeid=entity.eid):
+ return 0
+ elif not rschema.has_perm(entity._cw, self.action, toeid=entity.eid):
+ return 0
+ if self.target_etype is not None:
+ req = entity._cw
+ teschema = req.vreg.schema.eschema(self.target_etype)
+ if not teschema.may_have_permission('read', req):
+ return 0
+ return 1
+
+
+class partial_relation_possible(PartialPredicateMixIn, relation_possible):
+ """Same as :class:~`cubicweb.predicates.relation_possible`, but will look for
+ attributes of the selected class to get information which is otherwise
+ expected by the initializer, except for `action` and `strict` which are kept
+ as initializer arguments.
+
+ This is useful to predefine predicate of an abstract class designed to be
+ customized.
+ """
+ def __init__(self, action='read', **kwargs):
+ super(partial_relation_possible, self).__init__(None, None, None,
+ action, **kwargs)
+
+ def complete(self, cls):
+ self.rtype = cls.rtype
+ self.role = role(cls)
+ self.target_etype = getattr(cls, 'target_etype', None)
+
+
+class has_related_entities(EntityPredicate):
+ """Return 1 if entity support the specified relation and has some linked
+ entities by this relation , optionaly filtered according to the specified
+ target type.
+
+ The relation is specified by the following initializer arguments:
+
+ * `rtype`, the name of the relation
+
+ * `role`, the role of the entity in the relation, either 'subject' or
+ 'object', default to 'subject'.
+
+ * `target_etype`, optional name of an entity type that should be found
+ at the other end of the relation
+
+ See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
+ lookup / score rules according to the input context.
+ """
+ def __init__(self, rtype, role='subject', target_etype=None, **kwargs):
+ super(has_related_entities, self).__init__(**kwargs)
+ self.rtype = rtype
+ self.role = role
+ self.target_etype = target_etype
+
+ def score_entity(self, entity):
+ relpossel = relation_possible(self.rtype, self.role, self.target_etype)
+ if not relpossel.score_class(entity.__class__, entity._cw):
+ return 0
+ rset = entity.related(self.rtype, self.role)
+ if self.target_etype:
+ return any(r for r in rset.description if r[0] == self.target_etype)
+ return rset and 1 or 0
+
+
+class partial_has_related_entities(PartialPredicateMixIn, has_related_entities):
+ """Same as :class:~`cubicweb.predicates.has_related_entity`, but will look
+ for attributes of the selected class to get information which is otherwise
+ expected by the initializer.
+
+ This is useful to predefine predicate of an abstract class designed to be
+ customized.
+ """
+ def __init__(self, **kwargs):
+ super(partial_has_related_entities, self).__init__(None, None, None,
+ **kwargs)
+
+ def complete(self, cls):
+ self.rtype = cls.rtype
+ self.role = role(cls)
+ self.target_etype = getattr(cls, 'target_etype', None)
+
+
+class has_permission(EntityPredicate):
+ """Return non-zero score if request's user has the permission to do the
+ requested action on the entity. `action` is an entity schema action (eg one
+ of 'read', 'add', 'delete', 'update').
+
+ Here are entity lookup / scoring rules:
+
+ * if `entity` is specified, check permission is granted for this entity
+
+ * elif `row` is specified, check permission is granted for the entity found
+ in the specified cell
+
+ * else check permission is granted for each entity found in the column
+ specified specified by the `col` argument or in column 0
+ """
+ def __init__(self, action):
+ self.action = action
+
+ # 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'])
+ if rset is None:
+ return 0
+ if row is None:
+ score = 0
+ need_local_check = []
+ geteschema = req.vreg.schema.eschema
+ user = req.user
+ action = self.action
+ for etype in rset.column_types(0):
+ if etype in BASE_TYPES:
+ return 0
+ eschema = geteschema(etype)
+ if not user.matching_groups(eschema.get_groups(action)):
+ if eschema.has_local_role(action):
+ # have to ckeck local roles
+ need_local_check.append(eschema)
+ continue
+ else:
+ # even a local role won't be enough
+ return 0
+ score += 1
+ if need_local_check:
+ # check local role for entities of necessary types
+ for i, row in enumerate(rset):
+ if not rset.description[i][col] in need_local_check:
+ continue
+ # micro-optimisation instead of calling self.score(req,
+ # rset, i, col): rset may be large
+ if not rset.get_entity(i, col).cw_has_perm(action):
+ return 0
+ score += 1
+ return score
+ return self.score(req, rset, row, col)
+
+ def score_entity(self, entity):
+ if entity.cw_has_perm(self.action):
+ return 1
+ return 0
+
+
+class has_add_permission(EClassPredicate):
+ """Return 1 if request's user has the add permission on entity type
+ specified in the `etype` initializer argument, or according to entity found
+ in the input content if not specified.
+
+ It also check that then entity type is not a strict subobject (e.g. may only
+ be used as a composed of another entity).
+
+ See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
+ class lookup / score rules according to the input context when `etype` is
+ not specified.
+ """
+ def __init__(self, etype=None, **kwargs):
+ super(has_add_permission, self).__init__(**kwargs)
+ self.etype = etype
+
+ def __call__(self, cls, req, **kwargs):
+ if self.etype is None:
+ return super(has_add_permission, self).__call__(cls, req, **kwargs)
+ return self.score(cls, req, self.etype)
+
+ def score_class(self, eclass, req):
+ eschema = eclass.e_schema
+ if eschema.final or eschema.is_subobject(strict=True) \
+ or not eschema.has_perm(req, 'add'):
+ return 0
+ return 1
+
+
+class rql_condition(EntityPredicate):
+ """Return non-zero score if arbitrary rql specified in `expression`
+ initializer argument return some results for entity found in the input
+ context. Returned score is the number of items returned by the rql
+ condition.
+
+ `expression` is expected to be a string containing an rql expression, which
+ must use 'X' variable to represent the context entity and may use 'U' to
+ represent the request's user.
+
+ .. warning::
+ If simply testing value of some attribute/relation of context entity (X),
+ you should rather use the :class:`score_entity` predicate which will
+ benefit from the ORM's request entities cache.
+
+ See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
+ lookup / score rules according to the input context.
+ """
+ def __init__(self, expression, once_is_enough=None, mode='all', user_condition=False):
+ super(rql_condition, self).__init__(mode=mode, once_is_enough=once_is_enough)
+ self.user_condition = user_condition
+ if user_condition:
+ rql = 'Any COUNT(U) WHERE U eid %%(u)s, %s' % expression
+ elif 'U' in frozenset(split_expression(expression)):
+ rql = 'Any COUNT(X) WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression
+ else:
+ rql = 'Any COUNT(X) WHERE X eid %%(x)s, %s' % expression
+ self.rql = rql
+
+ def __str__(self):
+ return '%s(%r)' % (self.__class__.__name__, self.rql)
+
+ def __call__(self, cls, req, **kwargs):
+ if self.user_condition:
+ try:
+ return req.execute(self.rql, {'u': req.user.eid})[0][0]
+ except Unauthorized:
+ return 0
+ else:
+ return super(rql_condition, self).__call__(cls, req, **kwargs)
+
+ def _score(self, req, eid):
+ try:
+ return req.execute(self.rql, {'x': eid, 'u': req.user.eid})[0][0]
+ except Unauthorized:
+ return 0
+
+ def score(self, req, rset, row, col):
+ return self._score(req, rset[row][col])
+
+ def score_entity(self, entity):
+ return self._score(entity._cw, entity.eid)
+
+
+# workflow predicates ###########################################################
+
+class is_in_state(score_entity):
+ """Return 1 if entity is in one of the states given as argument list
+
+ You should use this instead of your own :class:`score_entity` predicate to
+ avoid some gotchas:
+
+ * possible views gives a fake entity with no state
+ * you must use the latest tr info thru the workflow adapter for repository
+ side checking of the current state
+
+ In debug mode, this predicate can raise :exc:`ValueError` for unknown states names
+ (only checked on entities without a custom workflow)
+
+ :rtype: int
+ """
+ def __init__(self, *expected):
+ assert expected, self
+ self.expected = frozenset(expected)
+ def score(entity, expected=self.expected):
+ adapted = entity.cw_adapt_to('IWorkflowable')
+ # in debug mode only (time consuming)
+ if entity._cw.vreg.config.debugmode:
+ # validation can only be done for generic etype workflow because
+ # expected transition list could have been changed for a custom
+ # workflow (for the current entity)
+ if not entity.custom_workflow:
+ self._validate(adapted)
+ return self._score(adapted)
+ super(is_in_state, self).__init__(score)
+
+ def _score(self, adapted):
+ trinfo = adapted.latest_trinfo()
+ if trinfo is None: # entity is probably in it's initial state
+ statename = adapted.state
+ else:
+ statename = trinfo.new_state.name
+ return statename in self.expected
+
+ def _validate(self, adapted):
+ wf = adapted.current_workflow
+ valid = [n.name for n in wf.reverse_state_of]
+ unknown = sorted(self.expected.difference(valid))
+ if unknown:
+ raise ValueError("%s: unknown state(s): %s"
+ % (wf.name, ",".join(unknown)))
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(str(s) for s in self.expected))
+
+
+def on_fire_transition(etype, tr_name, from_state_name=None):
+ """Return 1 when entity of the type `etype` is going through transition of
+ the name `tr_name`.
+
+ If `from_state_name` is specified, this predicate will also check the
+ incoming state.
+
+ You should use this predicate on 'after_add_entity' hook, since it's actually
+ looking for addition of `TrInfo` entities. Hence in the hook, `self.entity`
+ will reference the matching `TrInfo` entity, allowing to get all the
+ transition details (including the entity to which is applied the transition
+ but also its original state, transition, destination state, user...).
+
+ See :class:`cubicweb.entities.wfobjs.TrInfo` for more information.
+ """
+ def match_etype_and_transition(trinfo):
+ # take care trinfo.transition is None when calling change_state
+ return (trinfo.transition and trinfo.transition.name == tr_name
+ # is_instance() first two arguments are 'cls' (unused, so giving
+ # None is fine) and the request/session
+ and is_instance(etype)(None, trinfo._cw, entity=trinfo.for_entity))
+
+ return is_instance('TrInfo') & score_entity(match_etype_and_transition)
+
+
+class match_transition(ExpectedValuePredicate):
+ """Return 1 if `transition` argument is found in the input context which has
+ a `.name` attribute matching one of the expected names given to the
+ initializer.
+
+ This predicate is expected to be used to customise the status change form in
+ the web ui.
+ """
+ def __call__(self, cls, req, transition=None, **kwargs):
+ # XXX check this is a transition that apply to the object?
+ if transition is None:
+ treid = req.form.get('treid', None)
+ if treid:
+ transition = req.entity_from_eid(treid)
+ if transition is not None and getattr(transition, 'name', None) in self.expected:
+ return 1
+ return 0
+
+
+# logged user predicates ########################################################
+
+@objectify_predicate
+def no_cnx(cls, req, **kwargs):
+ """Return 1 if the web session has no connection set. This occurs when
+ anonymous access is not allowed and user isn't authenticated.
+
+ May only be used on the web side, not on the data repository side.
+ """
+ if not req.cnx:
+ return 1
+ return 0
+
+@objectify_predicate
+def authenticated_user(cls, req, **kwargs):
+ """Return 1 if the user is authenticated (e.g. not the anonymous user).
+
+ May only be used on the web side, not on the data repository side.
+ """
+ if req.session.anonymous_session:
+ return 0
+ return 1
+
+
+# XXX == ~ authenticated_user()
+def anonymous_user():
+ """Return 1 if the user is not authenticated (e.g. is the anonymous user).
+
+ May only be used on the web side, not on the data repository side.
+ """
+ return ~ authenticated_user()
+
+class match_user_groups(ExpectedValuePredicate):
+ """Return a non-zero score if request's user is in at least one of the
+ groups given as initializer argument. Returned score is the number of groups
+ in which the user is.
+
+ If the special 'owners' group is given and `rset` is specified in the input
+ context:
+
+ * if `row` is specified check the entity at the given `row`/`col` (default
+ to 0) is owned by the user
+
+ * else check all entities in `col` (default to 0) are owned by the user
+ """
+
+ def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+ if not getattr(req, 'cnx', True): # default to True for repo session instances
+ return 0
+ user = req.user
+ if user is None:
+ return int('guests' in self.expected)
+ score = user.matching_groups(self.expected)
+ if not score and 'owners' in self.expected and rset:
+ if row is not None:
+ if not user.owns(rset[row][col]):
+ return 0
+ score = 1
+ else:
+ score = all(user.owns(r[col]) for r in rset)
+ return score
+
+# Web request predicates ########################################################
+
+# XXX deprecate
+@objectify_predicate
+def primary_view(cls, req, view=None, **kwargs):
+ """Return 1 if:
+
+ * *no view is specified* in the input context
+
+ * a view is specified and its `.is_primary()` method return True
+
+ This predicate is usually used by contextual components that only want to
+ appears for the primary view of an entity.
+ """
+ if view is not None and not view.is_primary():
+ return 0
+ return 1
+
+
+@objectify_predicate
+def contextual(cls, req, view=None, **kwargs):
+ """Return 1 if view's contextual property is true"""
+ if view is not None and view.contextual:
+ return 1
+ return 0
+
+
+class match_view(ExpectedValuePredicate):
+ """Return 1 if a view is specified an as its registry id is in one of the
+ expected view id given to the initializer.
+ """
+ def __call__(self, cls, req, view=None, **kwargs):
+ if view is None or not view.__regid__ in self.expected:
+ return 0
+ return 1
+
+
+class match_context(ExpectedValuePredicate):
+
+ def __call__(self, cls, req, context=None, **kwargs):
+ if not context in self.expected:
+ return 0
+ return 1
+
+
+# XXX deprecate
+@objectify_predicate
+def match_context_prop(cls, req, context=None, **kwargs):
+ """Return 1 if:
+
+ * no `context` is specified in input context (take care to confusion, here
+ `context` refers to a string given as an argument to the input context...)
+
+ * specified `context` is matching the context property value for the
+ appobject using this predicate
+
+ * the appobject's context property value is None
+
+ This predicate is usually used by contextual components that want to appears
+ in a configurable place.
+ """
+ if context is None:
+ return 1
+ propval = req.property_value('%s.%s.context' % (cls.__registry__,
+ cls.__regid__))
+ if propval and context != propval:
+ return 0
+ return 1
+
+
+class match_search_state(ExpectedValuePredicate):
+ """Return 1 if the current request search state is in one of the expected
+ states given to the initializer.
+
+ Known search states are either 'normal' or 'linksearch' (eg searching for an
+ object to create a relation with another).
+
+ This predicate is usually used by action that want to appears or not according
+ to the ui search state.
+ """
+
+ def __call__(self, cls, req, **kwargs):
+ try:
+ if not req.search_state[0] in self.expected:
+ return 0
+ except AttributeError:
+ return 1 # class doesn't care about search state, accept it
+ return 1
+
+
+class match_form_params(ExpectedValuePredicate):
+ """Return non-zero score if parameter names specified as initializer
+ arguments are specified in request's form parameters.
+
+ Return a score corresponding to the number of expected parameters.
+
+ When multiple parameters are expected, all of them should be found in
+ the input context unless `mode` keyword argument is given to 'any',
+ in which case a single matching parameter is enough.
+ """
+
+ def _values_set(self, cls, req, **kwargs):
+ return frozenset(req.form)
+
+
+class match_edited_type(ExpectedValuePredicate):
+ """return non-zero if main edited entity type is the one specified as
+ initializer argument, or is among initializer arguments if `mode` == 'any'.
+ """
+
+ def _values_set(self, cls, req, **kwargs):
+ try:
+ return frozenset((req.form['__type:%s' % req.form['__maineid']],))
+ except KeyError:
+ return frozenset()
+
+
+class match_form_id(ExpectedValuePredicate):
+ """return non-zero if request form identifier is the one specified as
+ initializer argument, or is among initializer arguments if `mode` == 'any'.
+ """
+
+ def _values_set(self, cls, req, **kwargs):
+ try:
+ return frozenset((req.form['__form_id'],))
+ except KeyError:
+ return frozenset()
+
+
+class specified_etype_implements(is_instance):
+ """Return non-zero score if the entity type specified by an 'etype' key
+ searched in (by priority) input context kwargs and request form parameters
+ match a known entity type (case insensitivly), and it's associated entity
+ class is of one of the type(s) given to the initializer. If multiple
+ arguments are given, matching one of them is enough.
+
+ .. note:: as with :class:`~cubicweb.predicates.is_instance`, entity types
+ should be given as string and the score will reflect class
+ proximity so the most specific object will be selected.
+
+ This predicate is usually used by views holding entity creation forms (since
+ we've no result set to work on).
+ """
+
+ def __call__(self, cls, req, **kwargs):
+ try:
+ etype = kwargs['etype']
+ except KeyError:
+ try:
+ etype = req.form['etype']
+ except KeyError:
+ return 0
+ else:
+ # only check this is a known type if etype comes from req.form,
+ # else we want the error to propagate
+ try:
+ etype = req.vreg.case_insensitive_etypes[etype.lower()]
+ req.form['etype'] = etype
+ except KeyError:
+ return 0
+ score = self.score_class(req.vreg['etypes'].etype_class(etype), req)
+ if score:
+ eschema = req.vreg.schema.eschema(etype)
+ if eschema.has_local_role('add') or eschema.has_perm(req, 'add'):
+ return score
+ return 0
+
+
+class attribute_edited(EntityPredicate):
+ """Scores if the specified attribute has been edited This is useful for
+ selection of forms by the edit controller.
+
+ The initial use case is on a form, in conjunction with match_transition,
+ which will not score at edit time::
+
+ is_instance('Version') & (match_transition('ready') |
+ attribute_edited('publication_date'))
+ """
+ def __init__(self, attribute, once_is_enough=None, mode='all'):
+ super(attribute_edited, self).__init__(mode=mode, once_is_enough=once_is_enough)
+ self._attribute = attribute
+
+ def score_entity(self, entity):
+ return eid_param(role_name(self._attribute, 'subject'), entity.eid) in entity._cw.form
+
+
+# Other predicates ##############################################################
+
+class match_exception(ExpectedValuePredicate):
+ """Return 1 if exception given as `exc` in the input context is an instance
+ of one of the class given on instanciation of this predicate.
+ """
+ def __init__(self, *expected):
+ assert expected, self
+ # we want a tuple, not a set as done in the parent class
+ self.expected = expected
+
+ def __call__(self, cls, req, exc=None, **kwargs):
+ if exc is not None and isinstance(exc, self.expected):
+ return 1
+ return 0
+
+
+@objectify_predicate
+def debug_mode(cls, req, rset=None, **kwargs):
+ """Return 1 if running in debug mode."""
+ return req.vreg.config.debugmode and 1 or 0
--- a/pylintext.py Tue Jul 10 10:33:19 2012 +0200
+++ b/pylintext.py Tue Jul 10 15:07:52 2012 +0200
@@ -15,14 +15,14 @@
def cubicweb_transform(module):
- # handle objectify_selector decorator. Only look at module level functions,
- # should be enough
+ # handle objectify_predicate decorator (and its former name until bw compat
+ # is kept). Only look at module level functions, should be enough.
for assnodes in module.locals.values():
for node in assnodes:
if isinstance(node, scoped_nodes.Function) and node.decorators:
for decorator in node.decorators.nodes:
for infered in decorator.infer():
- if infered.name == 'objectify_selector':
+ if infered.name in ('objectify_predicate', 'objectify_selector'):
turn_function_to_class(node)
break
else:
--- a/req.py Tue Jul 10 10:33:19 2012 +0200
+++ b/req.py Tue Jul 10 15:07:52 2012 +0200
@@ -204,6 +204,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 +225,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 +419,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/schema.py Tue Jul 10 10:33:19 2012 +0200
+++ b/schema.py Tue Jul 10 15:07:52 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.
@@ -1247,13 +1247,9 @@
# XXX deprecated
-from yams.buildobjs import RichString
from yams.constraints import StaticVocabularyConstraint
-try: # for yams < 0.35
- RichString = class_moved(RichString)
-except TypeError:
- RichString = moved('yams.buildobjs', 'RichString')
+RichString = moved('yams.buildobjs', 'RichString')
StaticVocabularyConstraint = class_moved(StaticVocabularyConstraint)
FormatConstraint = class_moved(FormatConstraint)
--- a/schemas/base.py Tue Jul 10 10:33:19 2012 +0200
+++ b/schemas/base.py Tue Jul 10 15:07:52 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.
@@ -51,7 +51,9 @@
class EmailAddress(EntityType):
"""an electronic mail address associated to a short alias"""
__permissions__ = {
- 'read': ('managers', 'users', 'guests',), # XXX if P use_email X, U has_read_permission P
+ # application that wishes public email, or use it for something else
+ # than users (eg Company, Person), should explicitly change permissions
+ 'read': ('managers', ERQLExpression('U use_email X')),
'add': ('managers', 'users',),
'delete': ('managers', 'owners', ERQLExpression('P use_email X, U has_update_permission P')),
'update': ('managers', 'owners', ERQLExpression('P use_email X, U has_update_permission P')),
--- a/selectors.py Tue Jul 10 10:33:19 2012 +0200
+++ b/selectors.py Tue Jul 10 15:07:52 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,1604 +15,27 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-""".. _Selectors:
-Selectors
----------
-
-Using and combining existant selectors
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-You can combine selectors using the `&`, `|` and `~` operators.
-
-When two selectors are combined using the `&` operator, it means that
-both should return a positive score. On success, the sum of scores is
-returned.
-
-When two selectors are combined using the `|` operator, it means that
-one of them should return a positive score. On success, the first
-positive score is returned.
-
-You can also "negate" a selector by precedeing it by the `~` unary operator.
-
-Of course you can use parenthesis to balance expressions.
-
-Example
-~~~~~~~
-
-The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
-the blog entity itself.
-
-To do that, one defines a method on entity classes that returns the
-RSS stream url for a given entity. The default implementation on
-:class:`~cubicweb.entities.AnyEntity` (the generic entity class used
-as base for all others) and a specific implementation on `Blog` will
-do what we want.
-
-But when we have a result set containing several `Blog` entities (or
-different entities), we don't know on which entity to call the
-aforementioned method. In this case, we keep the generic behaviour.
-
-Hence we have two cases here, one for a single-entity rsets, the other for
-multi-entities rsets.
-
-In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
-
-.. sourcecode:: python
-
- class RSSIconBox(box.Box):
- ''' just display the RSS icon on uniform result set '''
- __select__ = box.Box.__select__ & non_final_entity()
-
-It takes into account:
-
-* the inherited selection criteria (one has to look them up in the class
- hierarchy to know the details)
-
-* :class:`~cubicweb.selectors.non_final_entity`, which filters on result sets
- containing non final entities (a 'final entity' being synonym for entity
- attributes type, eg `String`, `Int`, etc)
-
-This matches our second case. Hence we have to provide a specific component for
-the first case:
-
-.. sourcecode:: python
-
- class EntityRSSIconBox(RSSIconBox):
- '''just display the RSS icon on uniform result set for a single entity'''
- __select__ = RSSIconBox.__select__ & one_line_rset()
-
-Here, one adds the :class:`~cubicweb.selectors.one_line_rset` selector, which
-filters result sets of size 1. Thus, on a result set containing multiple
-entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
-selectable. However for a result set with one entity, the `EntityRSSIconBox`
-class will have a higher score than `RSSIconBox`, which is what we wanted.
-
-Of course, once this is done, you have to:
-
-* fill in the call method of `EntityRSSIconBox`
-
-* provide the default implementation of the method returning the RSS stream url
- on :class:`~cubicweb.entities.AnyEntity`
-
-* redefine this method on `Blog`.
-
-
-When to use selectors?
-~~~~~~~~~~~~~~~~~~~~~~
-
-Selectors are to be used whenever arises the need of dispatching on the shape or
-content of a result set or whatever else context (value in request form params,
-authenticated user groups, etc...). That is, almost all the time.
-
-Here is a quick example:
-
-.. sourcecode:: python
-
- class UserLink(component.Component):
- '''if the user is the anonymous user, build a link to login else a link
- to the connected user object with a logout link
- '''
- __regid__ = 'loggeduserlink'
-
- def call(self):
- if self._cw.session.anonymous_session:
- # display login link
- ...
- else:
- # display a link to the connected user object with a loggout link
- ...
-
-The proper way to implement this with |cubicweb| is two have two different
-classes sharing the same identifier but with different selectors so you'll get
-the correct one according to the context.
-
-.. sourcecode:: python
-
- class UserLink(component.Component):
- '''display a link to the connected user object with a loggout link'''
- __regid__ = 'loggeduserlink'
- __select__ = component.Component.__select__ & authenticated_user()
-
- def call(self):
- # display useractions and siteactions
- ...
-
- class AnonUserLink(component.Component):
- '''build a link to login'''
- __regid__ = 'loggeduserlink'
- __select__ = component.Component.__select__ & anonymous_user()
-
- def call(self):
- # display login link
- ...
-
-The big advantage, aside readability once you're familiar with the
-system, is that your cube becomes much more easily customizable by
-improving componentization.
-
-
-.. _CustomSelectors:
-
-Defining your own selectors
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. autodocstring:: cubicweb.appobject::objectify_selector
-
-In other cases, you can take a look at the following abstract base classes:
-
-.. autoclass:: cubicweb.selectors.ExpectedValueSelector
-.. autoclass:: cubicweb.selectors.EClassSelector
-.. autoclass:: cubicweb.selectors.EntitySelector
-
-Also, think to use the :func:`lltrace` decorator on your selector class' :meth:`__call__` method
-or below the :func:`objectify_selector` decorator of your selector function so it gets
-traceable when :class:`traced_selection` is activated (see :ref:`DebuggingSelectors`).
-
-.. autofunction:: cubicweb.appobject.lltrace
-
-.. note::
- Selectors __call__ should *always* return a positive integer, and shall never
- return `None`.
-
-
-.. _DebuggingSelectors:
-
-Debugging selection
-~~~~~~~~~~~~~~~~~~~
-
-Once in a while, one needs to understand why a view (or any application object)
-is, or is not selected appropriately. Looking at which selectors fired (or did
-not) is the way. The :class:`cubicweb.appobject.traced_selection` context
-manager to help with that, *if you're running your instance in debug mode*.
-
-.. autoclass:: cubicweb.appobject.traced_selection
-
-"""
-
-__docformat__ = "restructuredtext en"
-
-import logging
from warnings import warn
-from operator import eq
-
-from logilab.common.deprecation import class_renamed, deprecated
-from logilab.common.compat import all, any
-from logilab.common.interface import implements as implements_iface
-
-from yams.schema import BASE_TYPES, role_name
-from rql.nodes import Function
-
-from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity,
- CW_EVENT_MANAGER, role)
-# even if not used, let yes here so it's importable through this module
-from cubicweb.uilib import eid_param
-from cubicweb.appobject import Selector, objectify_selector, lltrace, yes
-from cubicweb.schema import split_expression
-
-from cubicweb.appobject import traced_selection # XXX for bw compat
-
-def score_interface(etypesreg, eclass, iface):
- """Return XXX if the give object (maybe an instance or class) implements
- the interface.
- """
- if getattr(iface, '__registry__', None) == 'etypes':
- # adjust score if the interface is an entity class
- parents, any = etypesreg.parent_classes(eclass.__regid__)
- if iface is eclass:
- return len(parents) + 4
- if iface is any: # Any
- return 1
- for index, basecls in enumerate(reversed(parents)):
- if iface is basecls:
- return index + 3
- return 0
- # XXX iface in implements deprecated in 3.9
- if implements_iface(eclass, iface):
- # implementing an interface takes precedence other special Any interface
- return 2
- return 0
-
-
-# abstract selectors / mixin helpers ###########################################
-
-class PartialSelectorMixIn(object):
- """convenience mix-in for selectors that will look into the containing
- class to find missing information.
-
- cf. `cubicweb.web.action.LinkToEntityAction` for instance
- """
- def __call__(self, cls, *args, **kwargs):
- self.complete(cls)
- return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs)
-
-
-class EClassSelector(Selector):
- """abstract class for selectors working on *entity class(es)* specified
- explicitly or found of the result set.
-
- Here are entity lookup / scoring rules:
-
- * if `entity` is specified, return score for this entity's class
-
- * elif `rset`, `select` and `filtered_variable` are specified, return score
- for the possible classes for variable in the given rql :class:`Select`
- node
-
- * elif `rset` and `row` are specified, return score for the class of the
- entity found in the specified cell, using column specified by `col` or 0
-
- * elif `rset` is specified return score for each entity class found in the
- column specified specified by the `col` argument or in column 0 if not
- specified
-
- When there are several classes to be evaluated, return the sum of scores for
- each entity class unless:
-
- - `mode` == 'all' (the default) and some entity class is scored
- to 0, in which case 0 is returned
-
- - `mode` == 'any', in which case the first non-zero score is
- returned
-
- - `accept_none` is False and some cell in the column has a None value
- (this may occurs with outer join)
- """
- def __init__(self, once_is_enough=None, accept_none=True, mode='all'):
- if once_is_enough is not None:
- warn("[3.14] once_is_enough is deprecated, use mode='any'",
- DeprecationWarning, stacklevel=2)
- if once_is_enough:
- mode = 'any'
- assert mode in ('any', 'all'), 'bad mode %s' % mode
- self.once_is_enough = mode == 'any'
- self.accept_none = accept_none
-
- @lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, entity=None,
- select=None, filtered_variable=None,
- accept_none=None,
- **kwargs):
- if entity is not None:
- return self.score_class(entity.__class__, req)
- if not rset:
- return 0
- if select is not None and filtered_variable is not None:
- etypes = set(sol[filtered_variable.name] for sol in select.solutions)
- elif row is None:
- if accept_none is None:
- accept_none = self.accept_none
- if not accept_none and \
- any(rset[i][col] is None for i in xrange(len(rset))):
- return 0
- etypes = rset.column_types(col)
- else:
- etype = rset.description[row][col]
- # may have None in rset.description on outer join
- if etype is None or rset.rows[row][col] is None:
- return 0
- etypes = (etype,)
- score = 0
- for etype in etypes:
- escore = self.score(cls, req, etype)
- if not escore and not self.once_is_enough:
- return 0
- elif self.once_is_enough:
- return escore
- score += escore
- return score
-
- def score(self, cls, req, etype):
- if etype in BASE_TYPES:
- return 0
- return self.score_class(req.vreg['etypes'].etype_class(etype), req)
-
- def score_class(self, eclass, req):
- raise NotImplementedError()
-
-
-class EntitySelector(EClassSelector):
- """abstract class for selectors working on *entity instance(s)* specified
- explicitly or found of the result set.
-
- Here are entity lookup / scoring rules:
-
- * if `entity` is specified, return score for this entity
-
- * elif `row` is specified, return score for the entity found in the
- specified cell, using column specified by `col` or 0
-
- * else return the sum of scores for each entity found in the column
- specified specified by the `col` argument or in column 0 if not specified,
- unless:
-
- - `mode` == 'all' (the default) and some entity class is scored
- to 0, in which case 0 is returned
-
- - `mode` == 'any', in which case the first non-zero score is
- returned
-
- - `accept_none` is False and some cell in the column has a None value
- (this may occurs with outer join)
-
- .. Note::
- using :class:`EntitySelector` or :class:`EClassSelector` as base selector
- class impacts performance, since when no entity or row is specified the
- later works on every different *entity class* found in the result set,
- while the former works on each *entity* (eg each row of the result set),
- which may be much more costly.
- """
- @lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
- **kwargs):
- if not rset and not kwargs.get('entity'):
- return 0
- score = 0
- if kwargs.get('entity'):
- score = self.score_entity(kwargs['entity'])
- elif row is None:
- col = col or 0
- if accept_none is None:
- accept_none = self.accept_none
- for row, rowvalue in enumerate(rset.rows):
- if rowvalue[col] is None: # outer join
- if not accept_none:
- return 0
- continue
- escore = self.score(req, rset, row, col)
- if not escore and not self.once_is_enough:
- return 0
- elif self.once_is_enough:
- return escore
- score += escore
- else:
- col = col or 0
- etype = rset.description[row][col]
- if etype is not None: # outer join
- score = self.score(req, rset, row, col)
- return score
-
- def score(self, req, rset, row, col):
- try:
- return self.score_entity(rset.get_entity(row, col))
- except NotAnEntity:
- return 0
-
- def score_entity(self, entity):
- raise NotImplementedError()
-
-
-class ExpectedValueSelector(Selector):
- """Take a list of expected values as initializer argument and store them
- into the :attr:`expected` set attribute. You may also give a set as single
- argument, which will then be referenced as set of expected values,
- allowing modifications to the given set to be considered.
-
- You should implement one of :meth:`_values_set(cls, req, **kwargs)` or
- :meth:`_get_value(cls, req, **kwargs)` method which should respectively
- return the set of values or the unique possible value for the given context.
-
- You may also specify a `mode` behaviour as argument, as explained below.
-
- Returned score is:
-
- - 0 if `mode` == 'all' (the default) and at least one expected
- values isn't found
-
- - 0 if `mode` == 'any' and no expected values isn't found at all
-
- - else the number of matching values
-
- Notice `mode` = 'any' with a single expected value has no effect at all.
- """
- def __init__(self, *expected, **kwargs):
- assert expected, self
- if len(expected) == 1 and isinstance(expected[0], set):
- self.expected = expected[0]
- else:
- self.expected = frozenset(expected)
- mode = kwargs.pop('mode', 'all')
- assert mode in ('any', 'all'), 'bad mode %s' % mode
- self.once_is_enough = mode == 'any'
- assert not kwargs, 'unexpected arguments %s' % kwargs
-
- def __str__(self):
- return '%s(%s)' % (self.__class__.__name__,
- ','.join(sorted(str(s) for s in self.expected)))
-
- @lltrace
- def __call__(self, cls, req, **kwargs):
- values = self._values_set(cls, req, **kwargs)
- matching = len(values & self.expected)
- if self.once_is_enough:
- return matching
- if matching == len(self.expected):
- return matching
- return 0
-
- def _values_set(self, cls, req, **kwargs):
- return frozenset( (self._get_value(cls, req, **kwargs),) )
-
- def _get_value(self, cls, req, **kwargs):
- raise NotImplementedError()
-
-
-# bare selectors ##############################################################
-
-class match_kwargs(ExpectedValueSelector):
- """Return non-zero score if parameter names specified as initializer
- arguments are specified in the input context.
-
-
- Return a score corresponding to the number of expected parameters.
-
- When multiple parameters are expected, all of them should be found in
- the input context unless `mode` keyword argument is given to 'any',
- in which case a single matching parameter is enough.
- """
-
- def _values_set(self, cls, req, **kwargs):
- return frozenset(kwargs)
-
-
-class appobject_selectable(Selector):
- """Return 1 if another appobject is selectable using the same input context.
-
- Initializer arguments:
-
- * `registry`, a registry name
-
- * `regids`, object identifiers in this registry, one of them should be
- selectable.
- """
- selectable_score = 1
- def __init__(self, registry, *regids):
- self.registry = registry
- self.regids = regids
-
- @lltrace
- def __call__(self, cls, req, **kwargs):
- for regid in self.regids:
- try:
- req.vreg[self.registry].select(regid, req, **kwargs)
- return self.selectable_score
- except NoSelectableObject:
- continue
- return 0
-
-
-class adaptable(appobject_selectable):
- """Return 1 if another appobject is selectable using the same input context.
-
- Initializer arguments:
-
- * `regids`, adapter identifiers (e.g. interface names) to which the context
- (usually entities) should be adaptable. One of them should be selectable
- when multiple identifiers are given.
- """
- def __init__(self, *regids):
- super(adaptable, self).__init__('adapters', *regids)
-
- def __call__(self, cls, req, **kwargs):
- kwargs.setdefault('accept_none', False)
- # being adaptable to an interface should takes precedence other is_instance('Any'),
- # but not other explicit is_instance('SomeEntityType'), and:
- # * is_instance('Any') score is 1
- # * is_instance('SomeEntityType') score is at least 2
- score = super(adaptable, self).__call__(cls, req, **kwargs)
- if score >= 2:
- return score - 0.5
- if score == 1:
- return score + 0.5
- return score
-
-
-class configuration_values(Selector):
- """Return 1 if the instance has an option set to a given value(s) in its
- configuration file.
- """
- # XXX this selector could be evaluated on startup
- def __init__(self, key, values):
- self._key = key
- if not isinstance(values, (tuple, list)):
- values = (values,)
- self._values = frozenset(values)
-
- @lltrace
- def __call__(self, cls, req, **kwargs):
- try:
- return self._score
- except AttributeError:
- if req is None:
- config = kwargs['repo'].config
- else:
- config = req.vreg.config
- self._score = config[self._key] in self._values
- return self._score
-
-
-# rset selectors ##############################################################
+from logilab.common.deprecation import deprecated, class_renamed
-@objectify_selector
-@lltrace
-def none_rset(cls, req, rset=None, **kwargs):
- """Return 1 if the result set is None (eg usually not specified)."""
- if rset is None:
- return 1
- return 0
-
-
-# XXX == ~ none_rset
-@objectify_selector
-@lltrace
-def any_rset(cls, req, rset=None, **kwargs):
- """Return 1 for any result set, whatever the number of rows in it, even 0."""
- if rset is not None:
- return 1
- return 0
-
-
-@objectify_selector
-@lltrace
-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:
- return 1
- return 0
-
-
-# XXX == ~ nonempty_rset
-@objectify_selector
-@lltrace
-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:
- return 1
- return 0
-
-
-# XXX == multi_lines_rset(1)
-@objectify_selector
-@lltrace
-def one_line_rset(cls, req, rset=None, row=None, **kwargs):
- """Return 1 if the result set is of size 1, or greater but a specific row in
- the result set is specified ('row' argument).
- """
- if rset is None and 'entity' in kwargs:
- return 1
- if rset is not None and (row is not None or rset.rowcount == 1):
- return 1
- return 0
-
-
-class multi_lines_rset(Selector):
- """Return 1 if the operator expression matches between `num` elements
- in the result set and the `expected` value if defined.
-
- By default, multi_lines_rset(expected) matches equality expression:
- `nb` row(s) in result set equals to expected value
- But, you can perform richer comparisons by overriding default operator:
- multi_lines_rset(expected, operator.gt)
-
- If `expected` is None, return 1 if the result set contains *at least*
- two rows.
- If rset is None, return 0.
- """
- def __init__(self, expected=None, operator=eq):
- self.expected = expected
- self.operator = operator
-
- def match_expected(self, num):
- if self.expected is None:
- return num > 1
- return self.operator(num, self.expected)
-
- @lltrace
- def __call__(self, cls, req, rset=None, **kwargs):
- return int(rset is not None and self.match_expected(rset.rowcount))
-
-
-class multi_columns_rset(multi_lines_rset):
- """If `nb` is specified, return 1 if the result set has exactly `nb` column
- per row. Else (`nb` is None), return 1 if the result set contains *at least*
- two columns per row. Return 0 for empty result set.
- """
-
- @lltrace
- def __call__(self, cls, req, rset=None, **kwargs):
- # '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(Selector):
- """Return 1 or more for result set with more rows than one or more page
- size. You can specify expected number of pages to the initializer (default
- to one), and you'll get that number of pages as score if the result set is
- big enough.
-
- Page size is searched in (respecting order):
- * a `page_size` argument
- * a `page_size` form parameters
- * the `navigation.page-size` property (see :ref:`PersistentProperties`)
- """
- def __init__(self, nbpages=1):
- assert nbpages > 0
- self.nbpages = nbpages
-
- @lltrace
- def __call__(self, cls, req, rset=None, **kwargs):
- if rset is None:
- return 0
- page_size = kwargs.get('page_size')
- if page_size is None:
- page_size = req.form.get('page_size')
- if page_size is None:
- page_size = req.property_value('navigation.page-size')
- else:
- page_size = int(page_size)
- if rset.rowcount <= (page_size*self.nbpages):
- return 0
- return self.nbpages
-
-
-@objectify_selector
-@lltrace
-def sorted_rset(cls, req, rset=None, **kwargs):
- """Return 1 for sorted result set (e.g. from an RQL query containing an
- ORDERBY clause), with exception that it will return 0 if the rset is
- 'ORDERBY FTIRANK(VAR)' (eg sorted by rank value of the has_text index).
- """
- if rset is None:
- return 0
- selects = rset.syntax_tree().children
- if (len(selects) > 1 or
- not selects[0].orderby or
- (isinstance(selects[0].orderby[0].term, Function) and
- selects[0].orderby[0].term.name == 'FTIRANK')
- ):
- return 0
- return 2
-
-
-# XXX == multi_etypes_rset(1)
-@objectify_selector
-@lltrace
-def one_etype_rset(cls, req, rset=None, col=0, **kwargs):
- """Return 1 if the result set contains entities which are all of the same
- type in the column specified by the `col` argument of the input context, or
- in column 0.
- """
- if rset is None:
- return 0
- if len(rset.column_types(col)) != 1:
- return 0
- return 1
-
-
-class multi_etypes_rset(multi_lines_rset):
- """If `nb` is specified, return 1 if the result set contains `nb` different
- types of entities in the column specified by the `col` argument of the input
- context, or in column 0. If `nb` is None, return 1 if the result set contains
- *at least* two different types of entities.
- """
-
- @lltrace
- def __call__(self, cls, req, rset=None, col=0, **kwargs):
- # 'or 0' since we *must not* return None
- return rset and self.match_expected(len(rset.column_types(col))) or 0
-
-
-@objectify_selector
-def logged_user_in_rset(cls, req, rset=None, row=None, col=0, **kwargs):
- """Return positive score if the result set at the specified row / col
- contains the eid of the logged user.
- """
- if rset is None:
- return 0
- return req.user.eid == rset[row or 0][col]
-
-
-# entity selectors #############################################################
-
-class non_final_entity(EClassSelector):
- """Return 1 for entity of a non final entity type(s). Remember, "final"
- entity types are String, Int, etc... This is equivalent to
- `is_instance('Any')` but more optimized.
-
- See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
- class lookup / score rules according to the input context.
- """
- def score(self, cls, req, etype):
- if etype in BASE_TYPES:
- return 0
- return 1
-
- def score_class(self, eclass, req):
- return 1 # necessarily true if we're there
+from cubicweb.predicates import *
-class implements(EClassSelector):
- """Return non-zero score for entity that are of the given type(s) or
- implements at least one of the given interface(s). If multiple arguments are
- given, matching one of them is enough.
-
- Entity types should be given as string, the corresponding class will be
- fetched from the entity types registry at selection time.
-
- See :class:`~cubicweb.selectors.EClassSelector` 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:: deprecated in cubicweb >= 3.9, use either
- :class:`~cubicweb.selectors.is_instance` or
- :class:`~cubicweb.selectors.adaptable`.
- """
-
- def __init__(self, *expected_ifaces, **kwargs):
- emit_warn = kwargs.pop('warn', True)
- super(implements, self).__init__(**kwargs)
- self.expected_ifaces = expected_ifaces
- if emit_warn:
- warn('[3.9] implements selector is deprecated, use either '
- 'is_instance or adaptable', DeprecationWarning, stacklevel=2)
-
- def __str__(self):
- return '%s(%s)' % (self.__class__.__name__,
- ','.join(str(s) for s in self.expected_ifaces))
-
- def score_class(self, eclass, req):
- score = 0
- etypesreg = req.vreg['etypes']
- for iface in self.expected_ifaces:
- if isinstance(iface, basestring):
- # entity type
- try:
- iface = etypesreg.etype_class(iface)
- except KeyError:
- continue # entity type not in the schema
- score += score_interface(etypesreg, eclass, iface)
- return score
-
-def _reset_is_instance_cache(vreg):
- vreg._is_instance_selector_cache = {}
-
-CW_EVENT_MANAGER.bind('before-registry-reset', _reset_is_instance_cache)
-
-class is_instance(EClassSelector):
- """Return non-zero score for entity that is an instance of the one of given
- type(s). If multiple arguments are given, matching one of them is enough.
-
- Entity types should be given as string, the corresponding class will be
- fetched from the registry at selection time.
-
- See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
- class lookup / score rules according to the input context.
-
- .. note:: the score will reflect class proximity so the most specific object
- will be selected.
- """
-
- def __init__(self, *expected_etypes, **kwargs):
- super(is_instance, self).__init__(**kwargs)
- self.expected_etypes = expected_etypes
- for etype in self.expected_etypes:
- assert isinstance(etype, basestring), etype
-
- def __str__(self):
- return '%s(%s)' % (self.__class__.__name__,
- ','.join(str(s) for s in self.expected_etypes))
-
- def score_class(self, eclass, req):
- # cache on vreg to avoid reloading issues
- try:
- cache = req.vreg._is_instance_selector_cache
- except AttributeError:
- # XXX 'before-registry-reset' not called for db-api connections
- cache = req.vreg._is_instance_selector_cache = {}
- try:
- expected_eclasses = cache[self]
- except KeyError:
- # turn list of entity types as string into a list of
- # (entity class, parent classes)
- etypesreg = req.vreg['etypes']
- expected_eclasses = cache[self] = []
- for etype in self.expected_etypes:
- try:
- expected_eclasses.append(etypesreg.etype_class(etype))
- except KeyError:
- continue # entity type not in the schema
- parents, any = req.vreg['etypes'].parent_classes(eclass.__regid__)
- score = 0
- for expectedcls in expected_eclasses:
- # adjust score according to class proximity
- if expectedcls is eclass:
- score += len(parents) + 4
- elif expectedcls is any: # Any
- score += 1
- else:
- for index, basecls in enumerate(reversed(parents)):
- if expectedcls is basecls:
- score += index + 3
- break
- return score
-
-
-class score_entity(EntitySelector):
- """Return score according to an arbitrary function given as argument which
- will be called with input content entity as argument.
-
- This is a very useful selector that will usually interest you since it
- allows a lot of things without having to write a specific selector.
-
- The function can return arbitrary value which will be casted to an integer
- value at the end.
-
- See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
- lookup / score rules according to the input context.
- """
- def __init__(self, scorefunc, once_is_enough=None, mode='all'):
- super(score_entity, self).__init__(mode=mode, once_is_enough=once_is_enough)
- def intscore(*args, **kwargs):
- score = scorefunc(*args, **kwargs)
- if not score:
- return 0
- if isinstance(score, (int, long)):
- return score
- return 1
- self.score_entity = intscore
-
-
-class has_mimetype(EntitySelector):
- """Return 1 if the entity adapt to IDownloadable and has the given MIME type.
-
- You can give 'image/' to match any image for instance, or 'image/png' to match
- only PNG images.
- """
- def __init__(self, mimetype, once_is_enough=None, mode='all'):
- super(has_mimetype, self).__init__(mode=mode, once_is_enough=once_is_enough)
- self.mimetype = mimetype
-
- def score_entity(self, entity):
- idownloadable = entity.cw_adapt_to('IDownloadable')
- if idownloadable is None:
- return 0
- mt = idownloadable.download_content_type()
- if not (mt and mt.startswith(self.mimetype)):
- return 0
- return 1
-
-
-class relation_possible(EntitySelector):
- """Return 1 for entity that supports the relation, provided that the
- request's user may do some `action` on it (see below).
-
- The relation is specified by the following initializer arguments:
-
- * `rtype`, the name of the relation
-
- * `role`, the role of the entity in the relation, either 'subject' or
- 'object', default to 'subject'
-
- * `target_etype`, optional name of an entity type that should be supported
- at the other end of the relation
-
- * `action`, a relation schema action (e.g. one of 'read', 'add', 'delete',
- default to 'read') which must be granted to the user, else a 0 score will
- be returned. Give None if you don't want any permission checking.
-
- * `strict`, boolean (default to False) telling what to do when the user has
- not globally the permission for the action (eg the action is not granted
- to one of the user's groups)
-
- - when strict is False, if there are some local role defined for this
- action (e.g. using rql expressions), then the permission will be
- considered as granted
-
- - when strict is True, then the permission will be actually checked for
- each entity
-
- Setting `strict` to True impacts performance for large result set since
- you'll then get the :class:`~cubicweb.selectors.EntitySelector` behaviour
- while otherwise you get the :class:`~cubicweb.selectors.EClassSelector`'s
- one. See those classes documentation for entity lookup / score rules
- according to the input context.
- """
-
- def __init__(self, rtype, role='subject', target_etype=None,
- action='read', strict=False, **kwargs):
- super(relation_possible, self).__init__(**kwargs)
- self.rtype = rtype
- self.role = role
- self.target_etype = target_etype
- self.action = action
- self.strict = strict
-
- # hack hack hack
- def __call__(self, cls, req, **kwargs):
- # hack hack hack
- if self.strict:
- return EntitySelector.__call__(self, cls, req, **kwargs)
- return EClassSelector.__call__(self, cls, req, **kwargs)
-
- def score(self, *args):
- if self.strict:
- return EntitySelector.score(self, *args)
- return EClassSelector.score(self, *args)
+warn('[3.15] cubicweb.selectors renamed into cubicweb.predicates',
+ DeprecationWarning, stacklevel=2)
- def _get_rschema(self, eclass):
- eschema = eclass.e_schema
- try:
- if self.role == 'object':
- return eschema.objrels[self.rtype]
- else:
- return eschema.subjrels[self.rtype]
- except KeyError:
- return None
-
- def score_class(self, eclass, req):
- rschema = self._get_rschema(eclass)
- if rschema is None:
- return 0 # relation not supported
- eschema = eclass.e_schema
- if self.target_etype is not None:
- try:
- rdef = rschema.role_rdef(eschema, self.target_etype, self.role)
- except KeyError:
- return 0
- if self.action and not rdef.may_have_permission(self.action, req):
- return 0
- teschema = req.vreg.schema.eschema(self.target_etype)
- if not teschema.may_have_permission('read', req):
- return 0
- elif self.action:
- return rschema.may_have_permission(self.action, req, eschema, self.role)
- return 1
-
- def score_entity(self, entity):
- rschema = self._get_rschema(entity)
- if rschema is None:
- return 0 # relation not supported
- if self.action:
- if self.target_etype is not None:
- rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role)
- if self.role == 'subject':
- if not rschema.has_perm(entity._cw, self.action, fromeid=entity.eid):
- return 0
- elif not rschema.has_perm(entity._cw, self.action, toeid=entity.eid):
- return 0
- if self.target_etype is not None:
- req = entity._cw
- teschema = req.vreg.schema.eschema(self.target_etype)
- if not teschema.may_have_permission('read', req):
- return 0
- return 1
-
-
-class partial_relation_possible(PartialSelectorMixIn, relation_possible):
- """Same as :class:~`cubicweb.selectors.relation_possible`, but will look for
- attributes of the selected class to get information which is otherwise
- expected by the initializer, except for `action` and `strict` which are kept
- as initializer arguments.
-
- This is useful to predefine selector of an abstract class designed to be
- customized.
- """
- def __init__(self, action='read', **kwargs):
- super(partial_relation_possible, self).__init__(None, None, None,
- action, **kwargs)
-
- def complete(self, cls):
- self.rtype = cls.rtype
- self.role = role(cls)
- self.target_etype = getattr(cls, 'target_etype', None)
-
-
-class has_related_entities(EntitySelector):
- """Return 1 if entity support the specified relation and has some linked
- entities by this relation , optionaly filtered according to the specified
- target type.
-
- The relation is specified by the following initializer arguments:
-
- * `rtype`, the name of the relation
-
- * `role`, the role of the entity in the relation, either 'subject' or
- 'object', default to 'subject'.
-
- * `target_etype`, optional name of an entity type that should be found
- at the other end of the relation
-
- See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
- lookup / score rules according to the input context.
- """
- def __init__(self, rtype, role='subject', target_etype=None, **kwargs):
- super(has_related_entities, self).__init__(**kwargs)
- self.rtype = rtype
- self.role = role
- self.target_etype = target_etype
-
- def score_entity(self, entity):
- relpossel = relation_possible(self.rtype, self.role, self.target_etype)
- if not relpossel.score_class(entity.__class__, entity._cw):
- return 0
- rset = entity.related(self.rtype, self.role)
- if self.target_etype:
- return any(r for r in rset.description if r[0] == self.target_etype)
- return rset and 1 or 0
-
-
-class partial_has_related_entities(PartialSelectorMixIn, has_related_entities):
- """Same as :class:~`cubicweb.selectors.has_related_entity`, but will look
- for attributes of the selected class to get information which is otherwise
- expected by the initializer.
-
- This is useful to predefine selector of an abstract class designed to be
- customized.
- """
- def __init__(self, **kwargs):
- super(partial_has_related_entities, self).__init__(None, None, None,
- **kwargs)
-
- def complete(self, cls):
- self.rtype = cls.rtype
- self.role = role(cls)
- self.target_etype = getattr(cls, 'target_etype', None)
-
-
-class has_permission(EntitySelector):
- """Return non-zero score if request's user has the permission to do the
- requested action on the entity. `action` is an entity schema action (eg one
- of 'read', 'add', 'delete', 'update').
-
- Here are entity lookup / scoring rules:
-
- * if `entity` is specified, check permission is granted for this entity
-
- * elif `row` is specified, check permission is granted for the entity found
- in the specified cell
-
- * else check permission is granted for each entity found in the column
- specified specified by the `col` argument or in column 0
- """
- def __init__(self, action):
- self.action = action
-
- # don't use EntitySelector.__call__ but this optimized implementation to
- # avoid considering each entity when it's not necessary
- @lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
- if kwargs.get('entity'):
- return self.score_entity(kwargs['entity'])
- if rset is None:
- return 0
- if row is None:
- score = 0
- need_local_check = []
- geteschema = req.vreg.schema.eschema
- user = req.user
- action = self.action
- for etype in rset.column_types(0):
- if etype in BASE_TYPES:
- return 0
- eschema = geteschema(etype)
- if not user.matching_groups(eschema.get_groups(action)):
- if eschema.has_local_role(action):
- # have to ckeck local roles
- need_local_check.append(eschema)
- continue
- else:
- # even a local role won't be enough
- return 0
- score += 1
- if need_local_check:
- # check local role for entities of necessary types
- for i, row in enumerate(rset):
- if not rset.description[i][col] in need_local_check:
- continue
- # micro-optimisation instead of calling self.score(req,
- # rset, i, col): rset may be large
- if not rset.get_entity(i, col).cw_has_perm(action):
- return 0
- score += 1
- return score
- return self.score(req, rset, row, col)
-
- def score_entity(self, entity):
- if entity.cw_has_perm(self.action):
- return 1
- return 0
-
-
-class has_add_permission(EClassSelector):
- """Return 1 if request's user has the add permission on entity type
- specified in the `etype` initializer argument, or according to entity found
- in the input content if not specified.
-
- It also check that then entity type is not a strict subobject (e.g. may only
- be used as a composed of another entity).
-
- See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
- class lookup / score rules according to the input context when `etype` is
- not specified.
- """
- def __init__(self, etype=None, **kwargs):
- super(has_add_permission, self).__init__(**kwargs)
- self.etype = etype
-
- @lltrace
- def __call__(self, cls, req, **kwargs):
- if self.etype is None:
- return super(has_add_permission, self).__call__(cls, req, **kwargs)
- return self.score(cls, req, self.etype)
-
- def score_class(self, eclass, req):
- eschema = eclass.e_schema
- if eschema.final or eschema.is_subobject(strict=True) \
- or not eschema.has_perm(req, 'add'):
- return 0
- return 1
-
-
-class rql_condition(EntitySelector):
- """Return non-zero score if arbitrary rql specified in `expression`
- initializer argument return some results for entity found in the input
- context. Returned score is the number of items returned by the rql
- condition.
+# XXX pre 3.15 bw compat
+from cubicweb.appobject import (objectify_selector, traced_selection,
+ lltrace, yes)
- `expression` is expected to be a string containing an rql expression, which
- must use 'X' variable to represent the context entity and may use 'U' to
- represent the request's user.
-
- .. warning::
- If simply testing value of some attribute/relation of context entity (X),
- you should rather use the :class:`score_entity` selector which will
- benefit from the ORM's request entities cache.
-
- See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
- lookup / score rules according to the input context.
- """
- def __init__(self, expression, once_is_enough=None, mode='all', user_condition=False):
- super(rql_condition, self).__init__(mode=mode, once_is_enough=once_is_enough)
- self.user_condition = user_condition
- if user_condition:
- rql = 'Any COUNT(U) WHERE U eid %%(u)s, %s' % expression
- elif 'U' in frozenset(split_expression(expression)):
- rql = 'Any COUNT(X) WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression
- else:
- rql = 'Any COUNT(X) WHERE X eid %%(x)s, %s' % expression
- self.rql = rql
-
- def __str__(self):
- return '%s(%r)' % (self.__class__.__name__, self.rql)
-
- @lltrace
- def __call__(self, cls, req, **kwargs):
- if self.user_condition:
- try:
- return req.execute(self.rql, {'u': req.user.eid})[0][0]
- except Unauthorized:
- return 0
- else:
- return super(rql_condition, self).__call__(cls, req, **kwargs)
-
- def _score(self, req, eid):
- try:
- return req.execute(self.rql, {'x': eid, 'u': req.user.eid})[0][0]
- except Unauthorized:
- return 0
-
- def score(self, req, rset, row, col):
- return self._score(req, rset[row][col])
-
- def score_entity(self, entity):
- return self._score(entity._cw, entity.eid)
-
-
-# workflow selectors ###########################################################
-
-class is_in_state(score_entity):
- """Return 1 if entity is in one of the states given as argument list
-
- You should use this instead of your own :class:`score_entity` selector to
- avoid some gotchas:
-
- * possible views gives a fake entity with no state
- * you must use the latest tr info thru the workflow adapter for repository
- side checking of the current state
-
- In debug mode, this selector can raise :exc:`ValueError` for unknown states names
- (only checked on entities without a custom workflow)
-
- :rtype: int
- """
- def __init__(self, *expected):
- assert expected, self
- self.expected = frozenset(expected)
- def score(entity, expected=self.expected):
- adapted = entity.cw_adapt_to('IWorkflowable')
- # in debug mode only (time consuming)
- if entity._cw.vreg.config.debugmode:
- # validation can only be done for generic etype workflow because
- # expected transition list could have been changed for a custom
- # workflow (for the current entity)
- if not entity.custom_workflow:
- self._validate(adapted)
- return self._score(adapted)
- super(is_in_state, self).__init__(score)
-
- def _score(self, adapted):
- trinfo = adapted.latest_trinfo()
- if trinfo is None: # entity is probably in it's initial state
- statename = adapted.state
- else:
- statename = trinfo.new_state.name
- return statename in self.expected
-
- def _validate(self, adapted):
- wf = adapted.current_workflow
- valid = [n.name for n in wf.reverse_state_of]
- unknown = sorted(self.expected.difference(valid))
- if unknown:
- raise ValueError("%s: unknown state(s): %s"
- % (wf.name, ",".join(unknown)))
-
- def __str__(self):
- return '%s(%s)' % (self.__class__.__name__,
- ','.join(str(s) for s in self.expected))
-
-
-def on_fire_transition(etype, tr_name, from_state_name=None):
- """Return 1 when entity of the type `etype` is going through transition of
- the name `tr_name`.
-
- If `from_state_name` is specified, this selector will also check the
- incoming state.
-
- You should use this selector on 'after_add_entity' hook, since it's actually
- looking for addition of `TrInfo` entities. Hence in the hook, `self.entity`
- will reference the matching `TrInfo` entity, allowing to get all the
- transition details (including the entity to which is applied the transition
- but also its original state, transition, destination state, user...).
-
- See :class:`cubicweb.entities.wfobjs.TrInfo` for more information.
- """
- def match_etype_and_transition(trinfo):
- # take care trinfo.transition is None when calling change_state
- return (trinfo.transition and trinfo.transition.name == tr_name
- # is_instance() first two arguments are 'cls' (unused, so giving
- # None is fine) and the request/session
- and is_instance(etype)(None, trinfo._cw, entity=trinfo.for_entity))
-
- return is_instance('TrInfo') & score_entity(match_etype_and_transition)
-
-
-class match_transition(ExpectedValueSelector):
- """Return 1 if `transition` argument is found in the input context which has
- a `.name` attribute matching one of the expected names given to the
- initializer.
-
- This selector is expected to be used to customise the status change form in
- the web ui.
- """
- @lltrace
- def __call__(self, cls, req, transition=None, **kwargs):
- # XXX check this is a transition that apply to the object?
- if transition is None:
- treid = req.form.get('treid', None)
- if treid:
- transition = req.entity_from_eid(treid)
- if transition is not None and getattr(transition, 'name', None) in self.expected:
- return 1
- return 0
-
-
-# logged user selectors ########################################################
-
-@objectify_selector
-@lltrace
-def no_cnx(cls, req, **kwargs):
- """Return 1 if the web session has no connection set. This occurs when
- anonymous access is not allowed and user isn't authenticated.
-
- May only be used on the web side, not on the data repository side.
- """
- if not req.cnx:
- return 1
- return 0
-
-@objectify_selector
-@lltrace
-def authenticated_user(cls, req, **kwargs):
- """Return 1 if the user is authenticated (e.g. not the anonymous user).
-
- May only be used on the web side, not on the data repository side.
- """
- if req.session.anonymous_session:
- return 0
- return 1
-
-
-# XXX == ~ authenticated_user()
-def anonymous_user():
- """Return 1 if the user is not authenticated (e.g. is the anonymous user).
-
- May only be used on the web side, not on the data repository side.
- """
- return ~ authenticated_user()
-
-class match_user_groups(ExpectedValueSelector):
- """Return a non-zero score if request's user is in at least one of the
- groups given as initializer argument. Returned score is the number of groups
- in which the user is.
-
- If the special 'owners' group is given and `rset` is specified in the input
- context:
-
- * if `row` is specified check the entity at the given `row`/`col` (default
- to 0) is owned by the user
-
- * else check all entities in `col` (default to 0) are owned by the user
- """
-
- @lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
- if not getattr(req, 'cnx', True): # default to True for repo session instances
- return 0
- user = req.user
- if user is None:
- return int('guests' in self.expected)
- score = user.matching_groups(self.expected)
- if not score and 'owners' in self.expected and rset:
- if row is not None:
- if not user.owns(rset[row][col]):
- return 0
- score = 1
- else:
- score = all(user.owns(r[col]) for r in rset)
- return score
-
-# Web request selectors ########################################################
+ExpectedValueSelector = class_renamed('ExpectedValueSelector',
+ ExpectedValuePredicate)
+EClassSelector = class_renamed('EClassSelector', EClassPredicate)
+EntitySelector = class_renamed('EntitySelector', EntityPredicate)
-# XXX deprecate
-@objectify_selector
-@lltrace
-def primary_view(cls, req, view=None, **kwargs):
- """Return 1 if:
-
- * *no view is specified* in the input context
-
- * a view is specified and its `.is_primary()` method return True
-
- This selector is usually used by contextual components that only want to
- appears for the primary view of an entity.
- """
- if view is not None and not view.is_primary():
- return 0
- return 1
-
-
-@objectify_selector
-@lltrace
-def contextual(cls, req, view=None, **kwargs):
- """Return 1 if view's contextual property is true"""
- if view is not None and view.contextual:
- return 1
- return 0
-
-
-class match_view(ExpectedValueSelector):
- """Return 1 if a view is specified an as its registry id is in one of the
- expected view id given to the initializer.
- """
- @lltrace
- def __call__(self, cls, req, view=None, **kwargs):
- if view is None or not view.__regid__ in self.expected:
- return 0
- return 1
-
-
-class match_context(ExpectedValueSelector):
-
- @lltrace
- def __call__(self, cls, req, context=None, **kwargs):
- if not context in self.expected:
- return 0
- return 1
-
-
-# XXX deprecate
-@objectify_selector
-@lltrace
-def match_context_prop(cls, req, context=None, **kwargs):
- """Return 1 if:
-
- * no `context` is specified in input context (take care to confusion, here
- `context` refers to a string given as an argument to the input context...)
-
- * specified `context` is matching the context property value for the
- appobject using this selector
-
- * the appobject's context property value is None
-
- This selector is usually used by contextual components that want to appears
- in a configurable place.
- """
- if context is None:
- return 1
- propval = req.property_value('%s.%s.context' % (cls.__registry__,
- cls.__regid__))
- if propval and context != propval:
- return 0
- return 1
-
-
-class match_search_state(ExpectedValueSelector):
- """Return 1 if the current request search state is in one of the expected
- states given to the initializer.
-
- Known search states are either 'normal' or 'linksearch' (eg searching for an
- object to create a relation with another).
-
- This selector is usually used by action that want to appears or not according
- to the ui search state.
- """
-
- @lltrace
- def __call__(self, cls, req, **kwargs):
- try:
- if not req.search_state[0] in self.expected:
- return 0
- except AttributeError:
- return 1 # class doesn't care about search state, accept it
- return 1
-
-
-class match_form_params(ExpectedValueSelector):
- """Return non-zero score if parameter names specified as initializer
- arguments are specified in request's form parameters.
-
- Return a score corresponding to the number of expected parameters.
-
- When multiple parameters are expected, all of them should be found in
- the input context unless `mode` keyword argument is given to 'any',
- in which case a single matching parameter is enough.
- """
-
- def _values_set(self, cls, req, **kwargs):
- return frozenset(req.form)
-
-
-class match_edited_type(ExpectedValueSelector):
- """return non-zero if main edited entity type is the one specified as
- initializer argument, or is among initializer arguments if `mode` == 'any'.
- """
-
- def _values_set(self, cls, req, **kwargs):
- try:
- return frozenset((req.form['__type:%s' % req.form['__maineid']],))
- except KeyError:
- return frozenset()
-
-
-class match_form_id(ExpectedValueSelector):
- """return non-zero if request form identifier is the one specified as
- initializer argument, or is among initializer arguments if `mode` == 'any'.
- """
-
- def _values_set(self, cls, req, **kwargs):
- try:
- return frozenset((req.form['__form_id'],))
- except KeyError:
- return frozenset()
-
-
-class specified_etype_implements(is_instance):
- """Return non-zero score if the entity type specified by an 'etype' key
- searched in (by priority) input context kwargs and request form parameters
- match a known entity type (case insensitivly), and it's associated entity
- class is of one of the type(s) given to the initializer. If multiple
- arguments are given, matching one of them is enough.
-
- .. note:: as with :class:`~cubicweb.selectors.is_instance`, entity types
- should be given as string and the score will reflect class
- proximity so the most specific object will be selected.
-
- This selector is usually used by views holding entity creation forms (since
- we've no result set to work on).
- """
-
- @lltrace
- def __call__(self, cls, req, **kwargs):
- try:
- etype = kwargs['etype']
- except KeyError:
- try:
- etype = req.form['etype']
- except KeyError:
- return 0
- else:
- # only check this is a known type if etype comes from req.form,
- # else we want the error to propagate
- try:
- etype = req.vreg.case_insensitive_etypes[etype.lower()]
- req.form['etype'] = etype
- except KeyError:
- return 0
- score = self.score_class(req.vreg['etypes'].etype_class(etype), req)
- if score:
- eschema = req.vreg.schema.eschema(etype)
- if eschema.has_local_role('add') or eschema.has_perm(req, 'add'):
- return score
- return 0
-
-
-class attribute_edited(EntitySelector):
- """Scores if the specified attribute has been edited This is useful for
- selection of forms by the edit controller.
-
- The initial use case is on a form, in conjunction with match_transition,
- which will not score at edit time::
-
- is_instance('Version') & (match_transition('ready') |
- attribute_edited('publication_date'))
- """
- def __init__(self, attribute, once_is_enough=None, mode='all'):
- super(attribute_edited, self).__init__(mode=mode, once_is_enough=once_is_enough)
- self._attribute = attribute
-
- def score_entity(self, entity):
- return eid_param(role_name(self._attribute, 'subject'), entity.eid) in entity._cw.form
-
-
-# Other selectors ##############################################################
-
-class match_exception(ExpectedValueSelector):
- """Return 1 if exception given as `exc` in the input context is an instance
- of one of the class given on instanciation of this predicate.
- """
- def __init__(self, *expected):
- assert expected, self
- # we want a tuple, not a set as done in the parent class
- self.expected = expected
-
- @lltrace
- def __call__(self, cls, req, exc=None, **kwargs):
- if exc is not None and isinstance(exc, self.expected):
- return 1
- return 0
-
-
-@objectify_selector
-def debug_mode(cls, req, rset=None, **kwargs):
- """Return 1 if running in debug mode."""
- return req.vreg.config.debugmode and 1 or 0
-
-
-## deprecated stuff ############################################################
+# XXX pre 3.7? bw compat
class on_transition(is_in_state):
@@ -1621,17 +44,17 @@
Especially useful to match passed transition to enable notifications when
your workflow allows several transition to the same states.
- Note that if workflow `change_state` adapter method is used, this selector
+ Note that if workflow `change_state` adapter method is used, this predicate
will not be triggered.
- You should use this instead of your own :class:`score_entity` selector to
+ You should use this instead of your own :class:`score_entity` predicate to
avoid some gotchas:
* possible views gives a fake entity with no state
* you must use the latest tr info thru the workflow adapter for repository
side checking of the current state
- In debug mode, this selector can raise:
+ In debug mode, this predicate can raise:
:raises: :exc:`ValueError` for unknown transition names
(etype workflow only not checked in custom workflow)
@@ -1655,12 +78,13 @@
raise ValueError("%s: unknown transition(s): %s"
% (wf.name, ",".join(unknown)))
+
entity_implements = class_renamed('entity_implements', is_instance)
-class _but_etype(EntitySelector):
+class _but_etype(EntityPredicate):
"""accept if the given entity types are not found in the result set.
- See `EntitySelector` documentation for behaviour when row is not specified.
+ See `EntityPredicate` documentation for behaviour when row is not specified.
:param *etypes: entity types (`basestring`) which should be refused
"""
@@ -1675,8 +99,7 @@
but_etype = class_renamed('but_etype', _but_etype, 'use ~is_instance(*etypes) instead')
-
-# XXX deprecated the one_* variants of selectors below w/ multi_xxx(nb=1)?
+# XXX deprecated the one_* variants of predicates below w/ multi_xxx(nb=1)?
# take care at the implementation though (looking for the 'row' argument's
# value)
two_lines_rset = class_renamed('two_lines_rset', multi_lines_rset)
--- a/server/__init__.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/__init__.py Tue Jul 10 15:07:52 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.
@@ -271,7 +292,9 @@
# available sources registry
SOURCE_TYPES = {'native': LazyObject('cubicweb.server.sources.native', 'NativeSQLSource'),
- 'pyrorql': LazyObject('cubicweb.server.sources.pyrorql', 'PyroRQLSource'),
+ 'datafeed': LazyObject('cubicweb.server.sources.datafeed', 'DataFeedSource'),
+ 'ldapfeed': LazyObject('cubicweb.server.sources.ldapfeed', 'LDAPFeedSource'),
'ldapuser': LazyObject('cubicweb.server.sources.ldapuser', 'LDAPUserSource'),
- 'datafeed': LazyObject('cubicweb.server.sources.datafeed', 'DataFeedSource'),
+ 'pyrorql': LazyObject('cubicweb.server.sources.pyrorql', 'PyroRQLSource'),
+ 'zmqrql': LazyObject('cubicweb.server.sources.zmqrql', 'ZMQRQLSource'),
}
--- a/server/checkintegrity.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/checkintegrity.py Tue Jul 10 15:07:52 2012 +0200
@@ -134,10 +134,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())
- # clear entity cache to avoid high memory consumption on big tables
- session.drop_entity_cache()
+ 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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/cwzmq.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,240 @@
+# -*- coding: utf-8 -*-
+# 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 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()
+
+class ZMQComm(object):
+ def __init__(self):
+ self.ioloop = ioloop.IOLoop()
+ self._topics = {}
+ self._subscribers = []
+ self.publisher = None
+
+ def add_publisher(self, address):
+ assert self.publisher is None, "more than one publisher is not supported"
+ self.publisher = Publisher(self.ioloop, address)
+
+ def add_subscription(self, topic, callback):
+ for subscriber in self._subscribers:
+ subscriber.subscribe(topic, callback)
+ self._topics[topic] = callback
+
+ def add_subscriber(self, address):
+ subscriber = Subscriber(self.ioloop, address)
+ for topic, callback in self._topics.iteritems():
+ subscriber.subscribe(topic, callback)
+ self._subscribers.append(subscriber)
+
+ def publish(self, msg):
+ assert self.publisher is not None, "can't publish without a publisher"
+ self.publisher.send(msg)
+
+ def start(self):
+ Thread(target=self.ioloop.start).start()
+
+ def stop(self):
+ self.ioloop.add_callback(self.ioloop.stop)
+
+ def __del__(self):
+ self.ioloop.close()
+
+
+class Publisher(object):
+ def __init__(self, ioloop, address):
+ self.address = address
+ self._topics = {}
+ self._subscribers = []
+ self.ioloop = ioloop
+ def callback():
+ s = ctx.socket(zmq.PUB)
+ self.stream = zmq.eventloop.zmqstream.ZMQStream(s, io_loop=ioloop)
+ self.stream.bind(self.address)
+ self.debug('start publisher on %s', self.address)
+ ioloop.add_callback(callback)
+
+ def send(self, msg):
+ self.ioloop.add_callback(lambda:self.stream.send_multipart(msg))
+
+
+class Subscriber(object):
+ def __init__(self, ioloop, address):
+ self.address = address
+ self.dispatch_table = {}
+ self.ioloop = ioloop
+ def callback():
+ s = ctx.socket(zmq.SUB)
+ self.stream = zmq.eventloop.zmqstream.ZMQStream(s, io_loop=ioloop)
+ self.stream.on_recv(self.dispatch)
+ self.stream.connect(self.address)
+ self.debug('start subscriber on %s', self.address)
+ ioloop.add_callback(callback)
+
+ def dispatch(self, msg):
+ try:
+ f = self.dispatch_table[msg[0]]
+ except KeyError:
+ return
+ f(msg)
+
+ def subscribe(self, topic, callback):
+ self.dispatch_table[topic] = callback
+ 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/hook.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/hook.py Tue Jul 10 15:07:52 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.
@@ -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.
@@ -233,7 +236,7 @@
or rollback() will restore the hooks.
-Hooks specific selector
+Hooks specific predicate
~~~~~~~~~~~~~~~~~~~~~~~
.. autoclass:: cubicweb.server.hook.match_rtype
.. autoclass:: cubicweb.server.hook.match_rtype_sets
@@ -258,13 +261,13 @@
from logilab.common.decorators import classproperty, cached
from logilab.common.deprecation import deprecated, class_renamed
from logilab.common.logging_ext import set_log_methods
+from logilab.common.registry import (Predicate, NotPredicate, OrPredicate,
+ classid, objectify_predicate, yes)
from cubicweb import RegistryNotFound
-from cubicweb.vregistry import classid
-from cubicweb.cwvreg import CWRegistry, VRegistry
-from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
- is_instance)
-from cubicweb.appobject import AppObject, NotSelector, OrSelector
+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',
@@ -273,7 +276,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
@@ -328,7 +332,7 @@
key=lambda x: x.order)
with security_enabled(session, 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
@@ -338,14 +342,15 @@
pruned hooks are the one which:
* are disabled at the session level
- * have a match_rtype or an is_instance selector which does not
- match the rtype / etype of the relations / entities for
- which we are calling the hooks. This works because the
- repository calls the hooks grouped by rtype or by etype when
- using the entities or eids_to_from keyword arguments
- Only hooks with a simple selector or an AndSelector of simple
- selectors are considered for disabling.
+ * have a selector containing a :class:`match_rtype` or an
+ :class:`is_instance` predicate which does not match the rtype / etype
+ of the relations / entities for which we are calling the hooks. This
+ works because the repository calls the hooks grouped by rtype or by
+ etype when using the entities or eids_to_from keyword arguments
+
+ Only hooks with a simple predicate or an AndPredicate of simple
+ predicates are considered for disabling.
"""
if 'entity' in kwargs:
@@ -410,24 +415,22 @@
for event in ALL_HOOKS:
- VRegistry.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
+ CWRegistryStore.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
@deprecated('[3.10] use entity.cw_edited.oldnewvalue(attr)')
def entity_oldnewvalue(entity, attr):
return entity.cw_edited.oldnewvalue(attr)
-# some hook specific selectors #################################################
+# some hook specific predicates #################################################
-@objectify_selector
-@lltrace
+@objectify_predicate
def enabled_category(cls, req, **kwargs):
if req is None:
return True # XXX how to deactivate server startup / shutdown event
return req.is_hook_activated(cls)
-@objectify_selector
-@lltrace
+@objectify_predicate
def from_dbapi_query(cls, req, **kwargs):
if req.running_dbapi_query:
return 1
@@ -440,9 +443,9 @@
return iter(chain(*self.iterators))
-class match_rtype(ExpectedValueSelector):
+class match_rtype(ExpectedValuePredicate):
"""accept if parameters specified as initializer arguments are specified
- in named arguments given to the selector
+ in named arguments given to the predicate
:param \*expected: parameters (eg `basestring`) which are expected to be
found in named arguments (kwargs)
@@ -453,7 +456,6 @@
self.toetypes = more.pop('toetypes', None)
assert not more, "unexpected kwargs in match_rtype: %s" % more
- @lltrace
def __call__(self, cls, req, *args, **kwargs):
if kwargs.get('rtype') not in self.expected:
return 0
@@ -466,10 +468,10 @@
return 1
-class match_rtype_sets(ExpectedValueSelector):
+class match_rtype_sets(ExpectedValuePredicate):
"""accept if the relation type is in one of the sets given as initializer
- argument. The goal of this selector is that it keeps reference to original sets,
- so modification to thoses sets are considered by the selector. For instance
+ 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()
@@ -489,7 +491,6 @@
def __init__(self, *expected):
self.expected = expected
- @lltrace
def __call__(self, cls, req, *args, **kwargs):
for rel_set in self.expected:
if kwargs.get('rtype') in rel_set:
@@ -535,7 +536,7 @@
@cached
def filterable_selectors(cls):
search = cls.__select__.search_selector
- if search((NotSelector, OrSelector)):
+ if search((NotPredicate, OrPredicate)):
return None, None
enabled_cat = search(enabled_category)
main_filter = search((is_instance, match_rtype))
@@ -583,7 +584,7 @@
Notice there are no default behaviour defined when a watched relation is
deleted, you'll have to handle this by yourself.
- You usually want to use the :class:`match_rtype_sets` selector on concrete
+ You usually want to use the :class:`match_rtype_sets` predicate on concrete
classes.
"""
events = ('after_add_relation',)
@@ -1067,6 +1068,8 @@
remove inserted eid from repository type/source cache
"""
try:
- self.session.repo.clear_caches(self.get_data())
+ eids = self.get_data()
+ self.session.repo.clear_caches(eids)
+ self.session.repo.app_instances_bus.publish(['delete'] + list(str(eid) for eid in eids))
except KeyError:
pass
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/ldaputils.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,362 @@
+# 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/>.
+"""cubicweb utilities for ldap sources
+
+Part of the code is coming form Zope's LDAPUserFolder
+
+Copyright (c) 2004 Jens Vagelpohl.
+All Rights Reserved.
+
+This software is subject to the provisions of the Zope Public License,
+Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+FOR A PARTICULAR PURPOSE.
+"""
+
+from __future__ import division # XXX why?
+
+import ldap
+from ldap.ldapobject import ReconnectLDAPObject
+from ldap.filter import filter_format
+from ldapurl import LDAPUrl
+
+from cubicweb import ValidationError, AuthenticationError, Binary
+from cubicweb.server.sources import ConnectionWrapper
+
+_ = unicode
+
+# search scopes
+BASE = ldap.SCOPE_BASE
+ONELEVEL = ldap.SCOPE_ONELEVEL
+SUBTREE = ldap.SCOPE_SUBTREE
+
+# map ldap protocol to their standard port
+PROTO_PORT = {'ldap': 389,
+ 'ldaps': 636,
+ 'ldapi': None,
+ }
+
+
+class LDAPSourceMixIn(object):
+ """a mix-in for LDAP based source"""
+ options = (
+ ('auth-mode',
+ {'type' : 'choice',
+ 'default': 'simple',
+ 'choices': ('simple', 'cram_md5', 'digest_md5', 'gssapi'),
+ 'help': 'authentication mode used to authenticate user to the ldap.',
+ 'group': 'ldap-source', 'level': 3,
+ }),
+ ('auth-realm',
+ {'type' : 'string',
+ 'default': None,
+ 'help': 'realm to use when using gssapi/kerberos authentication.',
+ 'group': 'ldap-source', 'level': 3,
+ }),
+
+ ('data-cnx-dn',
+ {'type' : 'string',
+ 'default': '',
+ 'help': 'user dn to use to open data connection to the ldap (eg used \
+to respond to rql queries). Leave empty for anonymous bind',
+ 'group': 'ldap-source', 'level': 1,
+ }),
+ ('data-cnx-password',
+ {'type' : 'string',
+ 'default': '',
+ 'help': 'password to use to open data connection to the ldap (eg used to respond to rql queries). Leave empty for anonymous bind.',
+ 'group': 'ldap-source', 'level': 1,
+ }),
+
+ ('user-base-dn',
+ {'type' : 'string',
+ 'default': 'ou=People,dc=logilab,dc=fr',
+ 'help': 'base DN to lookup for users',
+ 'group': 'ldap-source', 'level': 1,
+ }),
+ ('user-scope',
+ {'type' : 'choice',
+ 'default': 'ONELEVEL',
+ 'choices': ('BASE', 'ONELEVEL', 'SUBTREE'),
+ 'help': 'user search scope (valid values: "BASE", "ONELEVEL", "SUBTREE")',
+ 'group': 'ldap-source', 'level': 1,
+ }),
+ ('user-classes',
+ {'type' : 'csv',
+ 'default': ('top', 'posixAccount'),
+ 'help': 'classes of user (with Active Directory, you want to say "user" here)',
+ 'group': 'ldap-source', 'level': 1,
+ }),
+ ('user-filter',
+ {'type': 'string',
+ 'default': '',
+ 'help': 'additional filters to be set in the ldap query to find valid users',
+ 'group': 'ldap-source', 'level': 2,
+ }),
+ ('user-login-attr',
+ {'type' : 'string',
+ 'default': 'uid',
+ 'help': 'attribute used as login on authentication (with Active Directory, you want to use "sAMAccountName" here)',
+ 'group': 'ldap-source', 'level': 1,
+ }),
+ ('user-default-group',
+ {'type' : 'csv',
+ 'default': ('users',),
+ 'help': 'name of a group in which ldap users will be by default. \
+You can set multiple groups by separating them by a comma.',
+ 'group': 'ldap-source', 'level': 1,
+ }),
+ ('user-attrs-map',
+ {'type' : 'named',
+ '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,
+ }),
+
+ )
+
+ _conn = None
+
+ def _entity_update(self, source_entity):
+ if self.urls:
+ if len(self.urls) > 1:
+ raise ValidationError(source_entity, {'url': _('can only have one url')})
+ try:
+ protocol, hostport = self.urls[0].split('://')
+ except ValueError:
+ raise ValidationError(source_entity, {'url': _('badly formatted url')})
+ if protocol not in PROTO_PORT:
+ raise ValidationError(source_entity, {'url': _('unsupported protocol')})
+
+ def update_config(self, source_entity, typedconfig):
+ """update configuration from source entity. `typedconfig` is config
+ properly typed with defaults set
+ """
+ self.authmode = typedconfig['auth-mode']
+ self._authenticate = getattr(self, '_auth_%s' % self.authmode)
+ self.cnx_dn = typedconfig['data-cnx-dn']
+ self.cnx_pwd = typedconfig['data-cnx-password']
+ self.user_base_dn = str(typedconfig['user-base-dn'])
+ self.user_base_scope = globals()[typedconfig['user-scope']]
+ self.user_login_attr = typedconfig['user-login-attr']
+ self.user_default_groups = typedconfig['user-default-group']
+ self.user_attrs = typedconfig['user-attrs-map']
+ self.user_rev_attrs = {'eid': 'dn'}
+ for ldapattr, cwattr in self.user_attrs.items():
+ self.user_rev_attrs[cwattr] = ldapattr
+ self.base_filters = [filter_format('(%s=%s)', ('objectClass', o))
+ for o in typedconfig['user-classes']]
+ if typedconfig['user-filter']:
+ self.base_filters.append(typedconfig['user-filter'])
+ self._conn = None
+
+ def connection_info(self):
+ assert len(self.urls) == 1, self.urls
+ protocol, hostport = self.urls[0].split('://')
+ if protocol != 'ldapi' and not ':' in hostport:
+ hostport = '%s:%s' % (hostport, PROTO_PORT[protocol])
+ return protocol, hostport
+
+ def get_connection(self):
+ """open and return a connection to the source"""
+ if self._conn is None:
+ try:
+ self._connect()
+ except Exception:
+ self.exception('unable to connect to ldap')
+ return ConnectionWrapper(self._conn)
+
+ def authenticate(self, session, login, password=None, **kwargs):
+ """return CWUser eid for the given login/password if this account is
+ defined in this source, else raise `AuthenticationError`
+
+ two queries are needed since passwords are stored crypted, so we have
+ to fetch the salt first
+ """
+ self.info('ldap authenticate %s', login)
+ if not password:
+ # On Windows + ADAM this would have succeeded (!!!)
+ # You get Authenticated as: 'NT AUTHORITY\ANONYMOUS LOGON'.
+ # we really really don't want that
+ raise AuthenticationError()
+ searchfilter = [filter_format('(%s=%s)', (self.user_login_attr, login))]
+ searchfilter.extend(self.base_filters)
+ searchstr = '(&%s)' % ''.join(searchfilter)
+ # first search the user
+ try:
+ user = self._search(session, self.user_base_dn,
+ self.user_base_scope, searchstr)[0]
+ except (IndexError, ldap.SERVER_DOWN):
+ # no such user
+ raise AuthenticationError()
+ # check password by establishing a (unused) connection
+ try:
+ self._connect(user, password)
+ except ldap.LDAPError, ex:
+ # Something went wrong, most likely bad credentials
+ self.info('while trying to authenticate %s: %s', user, ex)
+ raise AuthenticationError()
+ except Exception:
+ self.error('while trying to authenticate %s', user, exc_info=True)
+ raise AuthenticationError()
+ eid = self.repo.extid2eid(self, user['dn'], 'CWUser', session, {})
+ if eid < 0:
+ # user has been moved away from this source
+ raise AuthenticationError()
+ return eid
+
+ def object_exists_in_ldap(self, dn):
+ cnx = self.get_connection().cnx #session.cnxset.connection(self.uri).cnx
+ if cnx is None:
+ 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:
+ self.warning('PARTIAL RESULTS for dn %s', dn)
+ except ldap.NO_SUCH_OBJECT:
+ return False
+ return True
+
+ def _connect(self, user=None, userpwd=None):
+ protocol, hostport = self.connection_info()
+ self.info('connecting %s://%s as %s', protocol, hostport,
+ user and user['dn'] or 'anonymous')
+ # don't require server certificate when using ldaps (will
+ # enable self signed certs)
+ ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
+ url = LDAPUrl(urlscheme=protocol, hostport=hostport)
+ conn = ReconnectLDAPObject(url.initializeUrl())
+ # Set the protocol version - version 3 is preferred
+ try:
+ conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)
+ 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
+ #conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
+ #conn.timeout = op_timeout
+ # Now bind with the credentials given. Let exceptions propagate out.
+ if user is None:
+ # no user specified, we want to initialize the 'data' connection,
+ assert self._conn is None
+ self._conn = conn
+ # XXX always use simple bind for data connection
+ if not self.cnx_dn:
+ conn.simple_bind_s(self.cnx_dn, self.cnx_pwd)
+ else:
+ self._authenticate(conn, {'dn': self.cnx_dn}, self.cnx_pwd)
+ else:
+ # user specified, we want to check user/password, no need to return
+ # the connection which will be thrown out
+ self._authenticate(conn, user, userpwd)
+ return conn
+
+ def _auth_simple(self, conn, user, userpwd):
+ conn.simple_bind_s(user['dn'], userpwd)
+
+ def _auth_cram_md5(self, conn, user, userpwd):
+ from ldap import sasl
+ auth_token = sasl.cram_md5(user['dn'], userpwd)
+ conn.sasl_interactive_bind_s('', auth_token)
+
+ def _auth_digest_md5(self, conn, user, userpwd):
+ from ldap import sasl
+ auth_token = sasl.digest_md5(user['dn'], userpwd)
+ conn.sasl_interactive_bind_s('', auth_token)
+
+ def _auth_gssapi(self, conn, user, userpwd):
+ # print XXX not proper sasl/gssapi
+ import kerberos
+ if not kerberos.checkPassword(user[self.user_login_attr], userpwd):
+ raise Exception('BAD login / mdp')
+ #from ldap import sasl
+ #conn.sasl_interactive_bind_s('', sasl.gssapi())
+
+ def _search(self, session, base, scope,
+ searchstr='(objectClass=*)', attrs=()):
+ """make an ldap query"""
+ self.debug('ldap search %s %s %s %s %s', self.uri, base, scope,
+ searchstr, list(attrs))
+ # XXX for now, we do not have connections set support for LDAP, so
+ # this is always self._conn
+ cnx = self.get_connection().cnx #session.cnxset.connection(self.uri).cnx
+ if cnx is None:
+ # cant connect to server
+ 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 []
+ try:
+ res = cnx.search_s(base, scope, searchstr, attrs)
+ except ldap.PARTIAL_RESULTS:
+ res = cnx.result(all=0)[1]
+ except ldap.NO_SUCH_OBJECT:
+ self.info('ldap NO SUCH OBJECT %s %s %s', base, scope, searchstr)
+ self._process_no_such_object(session, base)
+ return []
+ # except ldap.REFERRAL, e:
+ # cnx = self.handle_referral(e)
+ # try:
+ # res = cnx.search_s(base, scope, searchstr, attrs)
+ # except ldap.PARTIAL_RESULTS:
+ # res_type, res = cnx.result(all=0)
+ result = []
+ for rec_dn, rec_dict in res:
+ # When used against Active Directory, "rec_dict" may not be
+ # be a dictionary in some cases (instead, it can be a list)
+ #
+ # An example of a useless "res" entry that can be ignored
+ # from AD is
+ # (None, ['ldap://ForestDnsZones.PORTAL.LOCAL/DC=ForestDnsZones,DC=PORTAL,DC=LOCAL'])
+ # This appears to be some sort of internal referral, but
+ # we can't handle it, so we need to skip over it.
+ try:
+ items = rec_dict.iteritems()
+ except AttributeError:
+ continue
+ else:
+ itemdict = self._process_ldap_item(rec_dn, items)
+ result.append(itemdict)
+ #print '--->', result
+ self.debug('ldap built results %s', len(result))
+ return result
+
+ def _process_ldap_item(self, dn, iterator):
+ """Turn an ldap received item into a proper dict."""
+ itemdict = {'dn': dn}
+ for key, value in iterator:
+ 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):
+ """Some search return NO_SUCH_OBJECT error, handle this (usually because
+ an object whose dn is no more existent in ldap as been encountered).
+
+ Do nothing by default, let sub-classes handle that.
+ """
--- a/server/migractions.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/migractions.py Tue Jul 10 15:07:52 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.
@@ -51,14 +51,13 @@
from yams.schema import RelationDefinitionSchema
from cubicweb import CW_SOFTWARE_ROOT, AuthenticationError, ExecutionError
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.schema import (ETYPE_NAME_MAP, META_RTYPES, VIRTUAL_RTYPES,
PURE_VIRTUAL_RTYPES,
CubicWebRelationSchema, order_eschemas)
from cubicweb.cwvreg import CW_EVENT_MANAGER
from cubicweb.dbapi import get_repository, repo_connect
from cubicweb.migration import MigrationHelper, yes
-from cubicweb.server.session import hooks_control
from cubicweb.server import hook
try:
from cubicweb.server import SOURCE_TYPES, schemaserial as ss
@@ -152,7 +151,7 @@
elif options.backup_db:
self.backup_database(askconfirm=False)
# disable notification during migration
- with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, 'notification'):
+ with self.session.allow_all_hooks_but('notification'):
super(ServerMigrationHelper, self).migrate(vcconf, toupgrade, options)
def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs):
@@ -376,6 +375,9 @@
self.confirm = yes
self.execscript_confirm = yes
try:
+ if event == 'postcreate':
+ with self.session.allow_all_hooks_but():
+ return self.cmd_process_script(apc, funcname, *args, **kwargs)
return self.cmd_process_script(apc, funcname, *args, **kwargs)
finally:
self.confirm = confirm
@@ -698,8 +700,9 @@
str(totype))
# execute post-create files
for cube in reversed(newcubes):
- self.cmd_exec_event_script('postcreate', cube)
- self.commit()
+ with self.session.allow_all_hooks_but():
+ self.cmd_exec_event_script('postcreate', cube)
+ self.commit()
def cmd_remove_cube(self, cube, removedeps=False):
removedcubes = super(ServerMigrationHelper, self).cmd_remove_cube(
@@ -1465,7 +1468,7 @@
def rqliter(self, rql, kwargs=None, ask_confirm=True):
return ForRqlIterator(self, rql, kwargs, ask_confirm)
- # broken db commands ######################################################
+ # low-level commands to repair broken system database ######################
def cmd_change_attribute_type(self, etype, attr, newtype, commit=True):
"""low level method to change the type of an entity attribute. This is
--- a/server/mssteps.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/mssteps.py Tue Jul 10 15:07:52 2012 +0200
@@ -159,7 +159,9 @@
if self.outputtable:
self.plan.create_temp_table(self.outputtable)
sql = 'INSERT INTO %s %s' % (self.outputtable, sql)
- return self.plan.sqlexec(sql, self.plan.args)
+ self.plan.syssource.doexec(self.plan.session, sql, self.plan.args)
+ else:
+ return self.plan.sqlexec(sql, self.plan.args)
def get_sql(self):
self.inputmap = inputmap = self.children[-1].outputmap
--- a/server/repository.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/repository.py Tue Jul 10 15:07:52 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.
@@ -65,7 +65,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():
@@ -120,6 +120,21 @@
{'x': eidfrom, 'y': eidto})
+
+class NullEventBus(object):
+ def publish(self, msg):
+ pass
+
+ def add_subscription(self, topic, callback):
+ pass
+
+ def start(self):
+ pass
+
+ def stop(self):
+ pass
+
+
class Repository(object):
"""a repository provides access to a set of persistent storages for
entities and relations
@@ -127,18 +142,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.CubicWebVRegistry(config)
+ 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
@@ -162,6 +181,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):
@@ -232,8 +254,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, '
@@ -244,8 +265,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',
@@ -309,14 +328,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:
@@ -327,11 +345,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
@@ -340,15 +362,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.
@@ -356,15 +386,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):
@@ -392,21 +419,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()
@@ -436,8 +463,10 @@
"""validate authentication, raise AuthenticationError on failure, return
associated CWUser's eid on success.
"""
- for source in self.sources:
- if source.support_entity('CWUser'):
+ # iter on sources_by_uri then check enabled source since sources doesn't
+ # contain copy based sources
+ for source in self.sources_by_uri.itervalues():
+ if self.config.source_enabled(source) and source.support_entity('CWUser'):
try:
return source.authenticate(session, login, **authinfo)
except AuthenticationError:
@@ -497,7 +526,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
@@ -595,8 +625,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):
@@ -615,8 +644,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
@@ -637,14 +664,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
@@ -653,10 +677,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',
@@ -683,8 +706,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):
@@ -707,8 +728,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)
@@ -717,8 +737,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
@@ -730,13 +748,10 @@
raise `ConnectionError` if we can't open a connection
"""
# 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
+ cnxprops = kwargs.pop('cnxprops', None)
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()
@@ -861,6 +876,25 @@
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
@@ -930,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 ########################################################
@@ -1330,11 +1361,11 @@
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')
+ relations = []
+ activeintegrity = session.is_hook_category_activated('activeintegrity')
for attr in edited.iterkeys():
rschema = eschema.subjrels[attr]
if not rschema.final: # inlined relation
@@ -1342,7 +1373,7 @@
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:
+ if rdef.cardinality[1] in '1?' and activeintegrity:
with security_enabled(session, read=False):
session.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
{'x': entity.eid, 'y': value})
@@ -1654,6 +1685,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/server.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/server.py Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/server/serverconfig.py Tue Jul 10 15:07:52 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,6 +204,25 @@
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,
+ 'help': ('List of ZMQ addresses to subscribe to (requires pyzmq)'),
+ 'group': 'zmq', 'level': 1,
+ }),
+ ('zmq-address-pub',
+ {'type' : 'string',
+ 'default' : None,
+ 'help': ('ZMQ address to use for publishing (requires pyzmq)'),
+ 'group': 'zmq', 'level': 1,
+ }),
) + CubicWebConfiguration.options)
# should we init the connections pool (eg connect to sources). This is
--- a/server/serverctl.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/serverctl.py Tue Jul 10 15:07:52 2012 +0200
@@ -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 Tue Jul 10 10:33:19 2012 +0200
+++ b/server/session.py Tue Jul 10 15:07:52 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.
@@ -29,12 +29,12 @@
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.selectors import objectify_selector
from cubicweb.req import RequestSessionBase
from cubicweb.dbapi import ConnectionProperties
from cubicweb.utils import make_uid, RepeatList
@@ -74,23 +74,23 @@
except CoercionError:
return None
-@objectify_selector
+@objectify_predicate
def is_user_session(cls, req, **kwargs):
- """repository side only selector returning 1 if the session is a regular
+ """repository side only predicate returning 1 if the session is a regular
user session and not an internal session
"""
return not req.is_internal_session
-@objectify_selector
+@objectify_predicate
def is_internal_session(cls, req, **kwargs):
- """repository side only selector returning 1 if the session is not a regular
+ """repository side only predicate returning 1 if the session is not a regular
user session but an internal session
"""
return req.is_internal_session
-@objectify_selector
+@objectify_predicate
def repairing(cls, req, **kwargs):
- """repository side only selector returning 1 if the session is not a regular
+ """repository side only predicate returning 1 if the session is not a regular
user session but an internal session
"""
return req.vreg.config.repairing
@@ -252,13 +252,11 @@
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
@@ -847,6 +845,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 +894,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 +1122,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:
@@ -1272,6 +1275,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"""
--- a/server/sources/datafeed.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/sources/datafeed.py Tue Jul 10 15:07:52 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.
@@ -22,12 +22,12 @@
import urllib2
import StringIO
+from os.path import exists
from datetime import datetime, timedelta
from base64 import b64decode
from cookielib import CookieJar
from lxml import etree
-from logilab.mtconverter import xml_escape
from cubicweb import RegistryNotFound, ObjectNotFound, ValidationError, UnknownEid
from cubicweb.server.sources import AbstractSource
@@ -67,7 +67,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,
@@ -79,6 +79,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,
@@ -102,6 +103,7 @@
if url.strip()]
else:
self.urls = []
+
def update_config(self, source_entity, typedconfig):
"""update configuration from source entity. `typedconfig` is config
properly typed with defaults set
@@ -190,22 +192,13 @@
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 eid, etype in myuris.values():
- byetype.setdefault(etype, []).append(str(eid))
- self.error('delete %s entities %s', self.uri, byetype)
- for etype, eids in byetype.iteritems():
- 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'):
@@ -267,7 +260,7 @@
'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,
@@ -276,6 +269,7 @@
dataimport.init()
return dataimport
+
class DataFeedParser(AppObject):
__registry__ = 'parsers'
@@ -284,8 +278,14 @@
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
+ for mappedurl in URL_MAPPING:
+ if url.startswith(mappedurl):
+ return url.replace(mappedurl, URL_MAPPING[mappedurl], 1)
+ return url
def add_schema_config(self, schemacfg, checkonly=False):
"""added CWSourceSchemaConfig, modify mapping accordingly"""
@@ -339,7 +339,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
@@ -358,10 +358,43 @@
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
+ stuff in sub-classes.
+ """
+ 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.execute('DELETE %s X WHERE X eid IN (%s)'
+ % (etype, ','.join(eids)))
+
+ def update_if_necessary(self, entity, attrs):
+ 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)
+ 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)
@@ -383,30 +416,23 @@
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
- commit()
- 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))
- rollback()
- 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):
if url.startswith('http'):
- from cubicweb.sobjects.parsers import URL_MAPPING
- for mappedurl in URL_MAPPING:
- if url.startswith(mappedurl):
- url = url.replace(mappedurl, URL_MAPPING[mappedurl], 1)
- break
+ url = self.normalize_url(url)
self.source.info('GET %s', url)
stream = _OPENER.open(url)
elif url.startswith('file://'):
@@ -421,6 +447,17 @@
def process_item(self, *args):
raise NotImplementedError
+ def is_deleted(self, extid, etype, eid):
+ if extid.startswith('http'):
+ try:
+ _OPENER.open(self.normalize_url(extid)) # XXX HTTP HEAD request
+ except urllib2.HTTPError, ex:
+ if ex.code == 404:
+ return True
+ elif extid.startswith('file://'):
+ return exists(extid[7:])
+ return False
+
# use a cookie enabled opener to use session cookie if any
_OPENER = urllib2.build_opener()
try:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/sources/ldapfeed.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,45 @@
+# 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/>.
+"""cubicweb ldap feed source
+
+unlike ldapuser source, this source is copy based and will import ldap content
+(beside passwords for authentication) into the system source.
+"""
+
+from cubicweb.server.sources import datafeed
+from cubicweb.server import ldaputils
+
+
+class LDAPFeedSource(ldaputils.LDAPSourceMixIn,
+ datafeed.DataFeedSource):
+ """LDAP feed source"""
+ support_entities = {'CWUser': False}
+ use_cwuri_as_url = False
+
+ options = datafeed.DataFeedSource.options + ldaputils.LDAPSourceMixIn.options
+
+ def update_config(self, source_entity, typedconfig):
+ """update configuration from source entity. `typedconfig` is config
+ properly typed with defaults set
+ """
+ datafeed.DataFeedSource.update_config(self, source_entity, typedconfig)
+ ldaputils.LDAPSourceMixIn.update_config(self, source_entity, typedconfig)
+
+ def _entity_update(self, source_entity):
+ datafeed.DataFeedSource._entity_update(self, source_entity)
+ ldaputils.LDAPSourceMixIn._entity_update(self, source_entity)
--- a/server/sources/ldapuser.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/sources/ldapuser.py Tue Jul 10 15:07:52 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,33 +18,20 @@
"""cubicweb ldap user source
this source is for now limited to a read-only CWUser source
-
-Part of the code is coming form Zope's LDAPUserFolder
-
-Copyright (c) 2004 Jens Vagelpohl.
-All Rights Reserved.
-
-This software is subject to the provisions of the Zope Public License,
-Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
-THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-FOR A PARTICULAR PURPOSE.
"""
from __future__ import division
from base64 import b64decode
import ldap
-from ldap.ldapobject import ReconnectLDAPObject
-from ldap.filter import filter_format, escape_filter_chars
-from ldapurl import LDAPUrl
+from ldap.filter import escape_filter_chars
from rql.nodes import Relation, VariableRef, Constant, Function
-from cubicweb import AuthenticationError, UnknownEid, RepositoryError
+from cubicweb import UnknownEid, RepositoryError
+from cubicweb.server import ldaputils
from cubicweb.server.utils import cartesian_product
from cubicweb.server.sources import (AbstractSource, TrFunc, GlobTrFunc,
- ConnectionWrapper, TimedCache)
+ TimedCache)
# search scopes
BASE = ldap.SCOPE_BASE
@@ -58,97 +45,11 @@
}
-class LDAPUserSource(AbstractSource):
+class LDAPUserSource(ldaputils.LDAPSourceMixIn, AbstractSource):
"""LDAP read-only CWUser source"""
support_entities = {'CWUser': False}
- options = (
- ('host',
- {'type' : 'string',
- 'default': 'ldap',
- 'help': 'ldap host. It may contains port information using \
-<host>:<port> notation.',
- 'group': 'ldap-source', 'level': 1,
- }),
- ('protocol',
- {'type' : 'choice',
- 'default': 'ldap',
- 'choices': ('ldap', 'ldaps', 'ldapi'),
- 'help': 'ldap protocol (allowed values: ldap, ldaps, ldapi)',
- 'group': 'ldap-source', 'level': 1,
- }),
- ('auth-mode',
- {'type' : 'choice',
- 'default': 'simple',
- 'choices': ('simple', 'cram_md5', 'digest_md5', 'gssapi'),
- 'help': 'authentication mode used to authenticate user to the ldap.',
- 'group': 'ldap-source', 'level': 3,
- }),
- ('auth-realm',
- {'type' : 'string',
- 'default': None,
- 'help': 'realm to use when using gssapi/kerberos authentication.',
- 'group': 'ldap-source', 'level': 3,
- }),
-
- ('data-cnx-dn',
- {'type' : 'string',
- 'default': '',
- 'help': 'user dn to use to open data connection to the ldap (eg used \
-to respond to rql queries). Leave empty for anonymous bind',
- 'group': 'ldap-source', 'level': 1,
- }),
- ('data-cnx-password',
- {'type' : 'string',
- 'default': '',
- 'help': 'password to use to open data connection to the ldap (eg used to respond to rql queries). Leave empty for anonymous bind.',
- 'group': 'ldap-source', 'level': 1,
- }),
-
- ('user-base-dn',
- {'type' : 'string',
- 'default': 'ou=People,dc=logilab,dc=fr',
- 'help': 'base DN to lookup for users',
- 'group': 'ldap-source', 'level': 1,
- }),
- ('user-scope',
- {'type' : 'choice',
- 'default': 'ONELEVEL',
- 'choices': ('BASE', 'ONELEVEL', 'SUBTREE'),
- 'help': 'user search scope (valid values: "BASE", "ONELEVEL", "SUBTREE")',
- 'group': 'ldap-source', 'level': 1,
- }),
- ('user-classes',
- {'type' : 'csv',
- 'default': ('top', 'posixAccount'),
- 'help': 'classes of user (with Active Directory, you want to say "user" here)',
- 'group': 'ldap-source', 'level': 1,
- }),
- ('user-filter',
- {'type': 'string',
- 'default': '',
- 'help': 'additional filters to be set in the ldap query to find valid users',
- 'group': 'ldap-source', 'level': 2,
- }),
- ('user-login-attr',
- {'type' : 'string',
- 'default': 'uid',
- 'help': 'attribute used as login on authentication (with Active Directory, you want to use "sAMAccountName" here)',
- 'group': 'ldap-source', 'level': 1,
- }),
- ('user-default-group',
- {'type' : 'csv',
- 'default': ('users',),
- 'help': 'name of a group in which ldap users will be by default. \
-You can set multiple groups by separating them by a comma.',
- 'group': 'ldap-source', 'level': 1,
- }),
- ('user-attrs-map',
- {'type' : 'named',
- 'default': {'uid': 'login', 'gecos': 'email'},
- '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,
- }),
+ options = ldaputils.LDAPSourceMixIn.options + (
('synchronization-interval',
{'type' : 'time',
@@ -168,35 +69,32 @@
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))
- self._conn = None
+ self.update_config(None, self.check_conf_dict(eid, source_config,
+ fail_if_unknown=False))
+
+ def _entity_update(self, source_entity):
+ # XXX copy from datafeed source
+ if source_entity.url:
+ self.urls = [url.strip() for url in source_entity.url.splitlines()
+ if url.strip()]
+ else:
+ self.urls = []
+ # /end XXX
+ ldaputils.LDAPSourceMixIn._entity_update(self, source_entity)
def update_config(self, source_entity, typedconfig):
"""update configuration from source entity. `typedconfig` is config
properly typed with defaults set
"""
- self.host = typedconfig['host']
- self.protocol = typedconfig['protocol']
- self.authmode = typedconfig['auth-mode']
- self._authenticate = getattr(self, '_auth_%s' % self.authmode)
- self.cnx_dn = typedconfig['data-cnx-dn']
- self.cnx_pwd = typedconfig['data-cnx-password']
- self.user_base_dn = str(typedconfig['user-base-dn'])
- self.user_base_scope = globals()[typedconfig['user-scope']]
- self.user_login_attr = typedconfig['user-login-attr']
- self.user_default_groups = typedconfig['user-default-group']
- self.user_attrs = typedconfig['user-attrs-map']
- self.user_rev_attrs = {'eid': 'dn'}
- for ldapattr, cwattr in self.user_attrs.items():
- self.user_rev_attrs[cwattr] = ldapattr
- self.base_filters = [filter_format('(%s=%s)', ('objectClass', o))
- for o in typedconfig['user-classes']]
- if typedconfig['user-filter']:
- self.base_filters.append(typedconfig['user-filter'])
+ ldaputils.LDAPSourceMixIn.update_config(self, source_entity, typedconfig)
self._interval = typedconfig['synchronization-interval']
self._cache_ttl = max(71, typedconfig['cache-life-time'])
self.reset_caches()
- self._conn = None
+ # XXX copy from datafeed source
+ if source_entity is not None:
+ self._entity_update(source_entity)
+ self.config = typedconfig
+ # /end XXX
def reset_caches(self):
"""method called during test to reset potential source caches"""
@@ -207,21 +105,24 @@
"""method called by the repository once ready to handle request"""
if activated:
self.info('ldap init')
+ self._entity_update(source_entity)
# set minimum period of 5min 1s (the additional second is to
# minimize resonnance effet)
- self.repo.looping_task(max(301, self._interval), self.synchronize)
+ if self.user_rev_attrs['email']:
+ self.repo.looping_task(max(301, self._interval), self.synchronize)
self.repo.looping_task(self._cache_ttl // 10,
self._query_cache.clear_expired)
def synchronize(self):
+ self.pull_data(self.repo.internal_session())
+
+ def pull_data(self, session, force=False, raise_on_error=False):
"""synchronize content known by this repository with content in the
external repository
"""
self.info('synchronizing ldap source %s', self.uri)
- try:
- ldap_emailattr = self.user_rev_attrs['email']
- except KeyError:
- return # no email in ldap, we're done
+ ldap_emailattr = self.user_rev_attrs['email']
+ assert ldap_emailattr
session = self.repo.internal_session()
execute = session.execute
try:
@@ -268,54 +169,6 @@
session.commit()
session.close()
- def get_connection(self):
- """open and return a connection to the source"""
- if self._conn is None:
- try:
- self._connect()
- except Exception:
- self.exception('unable to connect to ldap:')
- return ConnectionWrapper(self._conn)
-
- def authenticate(self, session, login, password=None, **kwargs):
- """return CWUser eid for the given login/password if this account is
- defined in this source, else raise `AuthenticationError`
-
- two queries are needed since passwords are stored crypted, so we have
- to fetch the salt first
- """
- self.info('ldap authenticate %s', login)
- if not password:
- # On Windows + ADAM this would have succeeded (!!!)
- # You get Authenticated as: 'NT AUTHORITY\ANONYMOUS LOGON'.
- # we really really don't want that
- raise AuthenticationError()
- searchfilter = [filter_format('(%s=%s)', (self.user_login_attr, login))]
- searchfilter.extend(self.base_filters)
- searchstr = '(&%s)' % ''.join(searchfilter)
- # first search the user
- try:
- user = self._search(session, self.user_base_dn,
- self.user_base_scope, searchstr)[0]
- except IndexError:
- # no such user
- raise AuthenticationError()
- # check password by establishing a (unused) connection
- try:
- self._connect(user, password)
- except ldap.LDAPError, ex:
- # Something went wrong, most likely bad credentials
- self.info('while trying to authenticate %s: %s', user, ex)
- raise AuthenticationError()
- except Exception:
- self.error('while trying to authenticate %s', user, exc_info=True)
- raise AuthenticationError()
- eid = self.repo.extid2eid(self, user['dn'], 'CWUser', session)
- if eid < 0:
- # user has been moved away from this source
- raise AuthenticationError()
- return eid
-
def ldap_name(self, var):
if var.stinfo['relations']:
relname = iter(var.stinfo['relations']).next().r_type
@@ -383,7 +236,7 @@
except ldap.SERVER_DOWN:
# cant connect to server
msg = session._("can't connect to source %s, some data may be missing")
- session.set_shared_data('sources_error', msg % self.uri)
+ session.set_shared_data('sources_error', msg % self.uri, txdata=True)
return []
return results
@@ -459,127 +312,18 @@
#print '--> ldap result', result
return result
-
- def _connect(self, user=None, userpwd=None):
- if self.protocol == 'ldapi':
- hostport = self.host
- elif not ':' in self.host:
- hostport = '%s:%s' % (self.host, PROTO_PORT[self.protocol])
- else:
- hostport = self.host
- self.info('connecting %s://%s as %s', self.protocol, hostport,
- user and user['dn'] or 'anonymous')
- # don't require server certificate when using ldaps (will
- # enable self signed certs)
- ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
- url = LDAPUrl(urlscheme=self.protocol, hostport=hostport)
- conn = ReconnectLDAPObject(url.initializeUrl())
- # Set the protocol version - version 3 is preferred
- try:
- conn.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)
- 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
- #conn.set_option(ldap.OPT_NETWORK_TIMEOUT, conn_timeout)
- #conn.timeout = op_timeout
- # Now bind with the credentials given. Let exceptions propagate out.
- if user is None:
- # no user specified, we want to initialize the 'data' connection,
- assert self._conn is None
- self._conn = conn
- # XXX always use simple bind for data connection
- if not self.cnx_dn:
- conn.simple_bind_s(self.cnx_dn, self.cnx_pwd)
- else:
- self._authenticate(conn, {'dn': self.cnx_dn}, self.cnx_pwd)
- else:
- # user specified, we want to check user/password, no need to return
- # the connection which will be thrown out
- self._authenticate(conn, user, userpwd)
- return conn
-
- def _auth_simple(self, conn, user, userpwd):
- conn.simple_bind_s(user['dn'], userpwd)
-
- def _auth_cram_md5(self, conn, user, userpwd):
- from ldap import sasl
- auth_token = sasl.cram_md5(user['dn'], userpwd)
- conn.sasl_interactive_bind_s('', auth_token)
-
- def _auth_digest_md5(self, conn, user, userpwd):
- from ldap import sasl
- auth_token = sasl.digest_md5(user['dn'], userpwd)
- conn.sasl_interactive_bind_s('', auth_token)
+ def _process_ldap_item(self, dn, iterator):
+ itemdict = super(LDAPUserSource, self)._process_ldap_item(dn, iterator)
+ self._cache[dn] = itemdict
+ return itemdict
- def _auth_gssapi(self, conn, user, userpwd):
- # print XXX not proper sasl/gssapi
- import kerberos
- if not kerberos.checkPassword(user[self.user_login_attr], userpwd):
- raise Exception('BAD login / mdp')
- #from ldap import sasl
- #conn.sasl_interactive_bind_s('', sasl.gssapi())
-
- def _search(self, session, base, scope,
- searchstr='(objectClass=*)', attrs=()):
- """make an ldap query"""
- self.debug('ldap search %s %s %s %s %s', self.uri, base, scope,
- searchstr, list(attrs))
- # XXX for now, we do not have connections set support for LDAP, so
- # this is always self._conn
- cnx = session.cnxset.connection(self.uri).cnx
- try:
- res = cnx.search_s(base, scope, searchstr, attrs)
- except ldap.PARTIAL_RESULTS:
- res = cnx.result(all=0)[1]
- except ldap.NO_SUCH_OBJECT:
- self.info('ldap NO SUCH OBJECT')
- eid = self.repo.extid2eid(self, base, 'CWUser', session, insert=False)
- if eid:
- self.warning('deleting ldap user with eid %s and dn %s',
- eid, base)
- entity = session.entity_from_eid(eid, 'CWUser')
- self.repo.delete_info(session, entity, self.uri)
- self.reset_caches()
- return []
- # except ldap.REFERRAL, e:
- # cnx = self.handle_referral(e)
- # try:
- # res = cnx.search_s(base, scope, searchstr, attrs)
- # except ldap.PARTIAL_RESULTS:
- # res_type, res = cnx.result(all=0)
- result = []
- for rec_dn, rec_dict in res:
- # When used against Active Directory, "rec_dict" may not be
- # be a dictionary in some cases (instead, it can be a list)
- # An example of a useless "res" entry that can be ignored
- # from AD is
- # (None, ['ldap://ForestDnsZones.PORTAL.LOCAL/DC=ForestDnsZones,DC=PORTAL,DC=LOCAL'])
- # This appears to be some sort of internal referral, but
- # we can't handle it, so we need to skip over it.
- try:
- items = rec_dict.items()
- except AttributeError:
- # 'items' not found on rec_dict, skip
- continue
- for key, value in items: # XXX syt: huuum ?
- 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:
- rec_dict[key] = value = value[0]
- rec_dict['dn'] = rec_dn
- self._cache[rec_dn] = rec_dict
- result.append(rec_dict)
- #print '--->', result
- self.debug('ldap built results %s', len(result))
- return result
+ def _process_no_such_object(self, session, dn):
+ eid = self.repo.extid2eid(self, dn, 'CWUser', session, insert=False)
+ if eid:
+ self.warning('deleting ldap user with eid %s and dn %s', eid, dn)
+ entity = session.entity_from_eid(eid, 'CWUser')
+ self.repo.delete_info(session, entity, self.uri)
+ self.reset_caches()
def before_entity_insertion(self, session, lid, etype, eid, sourceparams):
"""called by the repository when an eid has been attributed for an
@@ -604,13 +348,13 @@
self.debug('ldap after entity insertion')
super(LDAPUserSource, self).after_entity_insertion(
session, lid, entity, sourceparams)
- dn = lid
for group in self.user_default_groups:
session.execute('SET X in_group G WHERE X eid %(x)s, G name %(group)s',
{'x': entity.eid, 'group': group})
# search for existant email first
try:
- emailaddr = self._cache[dn][self.user_rev_attrs['email']]
+ # lid = dn
+ emailaddr = self._cache[lid][self.user_rev_attrs['email']]
except KeyError:
return
if isinstance(emailaddr, list):
@@ -632,6 +376,7 @@
"""delete an entity from the source"""
raise RepositoryError('this source is read only')
+
def _insert_email(session, emailaddr, ueid):
session.execute('INSERT EmailAddress X: X address %(addr)s, U primary_email X '
'WHERE U eid %(x)s', {'addr': emailaddr, 'x': ueid})
--- a/server/sources/native.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/sources/native.py Tue Jul 10 15:07:52 2012 +0200
@@ -55,7 +55,7 @@
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
@@ -65,7 +65,6 @@
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.")
@@ -637,7 +636,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 +644,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 +656,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 +671,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 +711,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 +1156,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 +1220,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 +1281,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 +1305,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 +1365,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 +1388,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
--- a/server/sources/pyrorql.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/sources/pyrorql.py Tue Jul 10 15:07:52 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.
@@ -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,5 @@
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)
- 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)
- 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)
- 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 Jul 10 15:07:52 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, 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]
+
+ 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/server/sources/rql2sql.py Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/server/sources/storages.py Tue Jul 10 15:07:52 2012 +0200
@@ -18,6 +18,7 @@
"""custom storages for the system source"""
import os
+import sys
from os import unlink, path as osp
from contextlib import contextmanager
@@ -112,11 +113,29 @@
class BytesFileSystemStorage(Storage):
"""store Bytes attribute value on the file system"""
- def __init__(self, defaultdir, fsencoding='utf-8'):
+ def __init__(self, defaultdir, fsencoding='utf-8', wmode=0444):
if type(defaultdir) is unicode:
defaultdir = defaultdir.encode(fsencoding)
self.default_directory = defaultdir
self.fsencoding = fsencoding
+ # extra umask to use when creating file
+ # 0444 as in "only allow read bit in permission"
+ self._wmode = wmode
+
+ def _writecontent(self, path, binary):
+ """write the content of a binary in readonly file
+
+ As the bfss never alter a create file it does not prevent it to work as
+ intended. This is a beter safe than sorry approach.
+ """
+ write_flag = os.O_WRONLY | os.O_CREAT | os.O_EXCL
+ if sys.platform == 'win32':
+ write_flag |= os.O_BINARY
+ fd = os.open(path, write_flag, self._wmode)
+ fileobj = os.fdopen(fd, 'wb')
+ binary.to_file(fileobj)
+ fileobj.close()
+
def callback(self, source, session, value):
"""sql generator callback when some attribute with a custom storage is
@@ -138,7 +157,7 @@
fpath = self.new_fs_path(entity, attr)
# bytes storage used to store file's path
entity.cw_edited.edited_attribute(attr, Binary(fpath))
- binary.to_file(fpath)
+ self._writecontent(fpath, binary)
AddFileOp.get_instance(entity._cw).add_data(fpath)
return binary
@@ -171,7 +190,7 @@
fpath = self.new_fs_path(entity, attr)
assert not osp.exists(fpath)
# write attribute value on disk
- binary.to_file(fpath)
+ self._writecontent(fpath, binary)
# Mark the new file as added during the transaction.
# The file will be removed on rollback
AddFileOp.get_instance(entity._cw).add_data(fpath)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/sources/zmqrql.py Tue Jul 10 15:07:52 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/sqlutils.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/sqlutils.py Tue Jul 10 15:07:52 2012 +0200
@@ -214,31 +214,11 @@
# callback lookup for each *cell* in results when there is nothing to
# lookup
if not column_callbacks:
- return self._process_result(cursor)
+ return self.dbhelper.dbapi_module.process_cursor(cursor, self._dbencoding,
+ Binary)
assert session
return self._cb_process_result(cursor, column_callbacks, session)
- def _process_result(self, cursor):
- # begin bind to locals for optimization
- descr = cursor.description
- encoding = self._dbencoding
- process_value = self._process_value
- binary = Binary
- # /end
- cursor.arraysize = 100
- while True:
- results = cursor.fetchmany()
- if not results:
- break
- for line in results:
- result = []
- for col, value in enumerate(line):
- if value is None:
- result.append(value)
- continue
- result.append(process_value(value, descr[col], encoding, binary))
- yield result
-
def _cb_process_result(self, cursor, column_callbacks, session):
# begin bind to locals for optimization
descr = cursor.description
--- a/server/test/data/slapd.conf.in Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/data/slapd.conf.in Tue Jul 10 15:07:52 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_datafeed.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/unittest_datafeed.py Tue Jul 10 15:07:52 2012 +0200
@@ -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_ldapuser.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/unittest_ldapuser.py Tue Jul 10 15:07:52 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.
@@ -16,100 +16,222 @@
# 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
+
+from cubicweb import AuthenticationError
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.devtools.repotest import RQLGeneratorTC
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.session import security_enabled
+from cubicweb.server.sources.ldapuser import GlobTrFunc, UnknownEid, RQL2LDAPFilter
-CONFIG = u'''host=%s
-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, CONFIG
+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()))
- CONFIG = CONFIG % host
+ 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 DeleteStuffFromLDAPFeedSourceTC(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)
+
+ def _pull(self):
+ with self.session.repo.internal_session() as isession:
+ with security_enabled(isession, read=False, write=False):
+ 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')
+
+class LDAPFeedSourceTC(LDAPTestBase):
+ test_db_id = 'ldap-feed'
-class LDAPUserSourceTC(CubicWebTC):
+ @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)
+
+ def setUp(self):
+ super(LDAPFeedSourceTC, self).setUp()
+ # ldap source url in the database may use a different port as the one
+ # just attributed
+ lfsource = self.repo.sources_by_uri['ldapuser']
+ lfsource.urls = [URL]
+
+ def assertMetadata(self, entity):
+ self.assertTrue(entity.creation_date)
+ self.assertTrue(entity.modification_date)
+
+ def test_authenticate(self):
+ source = self.repo.sources_by_uri['ldapuser']
+ self.session.set_cnxset()
+ # ensure we won't be logged against
+ self.assertRaises(AuthenticationError,
+ source.authenticate, self.session, 'toto', 'toto')
+ self.assertTrue(source.authenticate(self.session, 'syt', 'syt'))
+ self.assertTrue(self.repo.connect('syt', password='syt'))
+
+ def test_base(self):
+ # check a known one
+ rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
+ e = rset.get_entity(0, 0)
+ self.assertEqual(e.login, 'syt')
+ e.complete()
+ self.assertMetadata(e)
+ self.assertEqual(e.firstname, None)
+ self.assertEqual(e.surname, None)
+ self.assertEqual(e.in_group[0].name, 'users')
+ self.assertEqual(e.owned_by[0].login, 'syt')
+ self.assertEqual(e.created_by, ())
+ self.assertEqual(e.primary_email[0].address, 'Sylvain Thenault')
+ # email content should be indexed on the user
+ rset = self.sexecute('CWUser X WHERE X has_text "thenault"')
+ self.assertEqual(rset.rows, [[e.eid]])
+
+ def test_copy_to_system_source(self):
+ source = self.repo.sources_by_uri['ldapuser']
+ eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
+ self.sexecute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': eid})
+ self.commit()
+ source.reset_caches()
+ rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
+ 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},
+ 'type': 'CWUser',
+ 'extid': None})
+ self.assertEqual(e.cw_source[0].name, 'system')
+ self.assertTrue(e.creation_date)
+ self.assertTrue(e.modification_date)
+ 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):
test_db_id = 'ldap-user'
tags = CubicWebTC.tags | Tags(('ldap'))
@classmethod
def pre_setup_database(cls, session, config):
session.create_entity('CWSource', name=u'ldapuser', type=u'ldapuser',
- config=CONFIG)
+ url=URL, config=CONFIG)
session.commit()
# XXX keep it there
session.execute('CWUser U')
- def test_authenticate(self):
- source = self.repo.sources_by_uri['ldapuser']
- self.session.set_cnxset()
- self.assertRaises(AuthenticationError,
- source.authenticate, self.session, 'toto', 'toto')
+ def assertMetadata(self, entity):
+ self.assertEqual(entity.creation_date, None)
+ self.assertEqual(entity.modification_date, None)
def test_synchronize(self):
source = self.repo.sources_by_uri['ldapuser']
@@ -121,8 +243,7 @@
e = rset.get_entity(0, 0)
self.assertEqual(e.login, 'syt')
e.complete()
- self.assertEqual(e.creation_date, None)
- self.assertEqual(e.modification_date, None)
+ self.assertMetadata(e)
self.assertEqual(e.firstname, None)
self.assertEqual(e.surname, None)
self.assertEqual(e.in_group[0].name, 'users')
@@ -347,27 +468,6 @@
rset = cu.execute('Any F WHERE X has_text "iaminguestsgrouponly", X firstname F')
self.assertEqual(rset.rows, [[None]])
- def test_copy_to_system_source(self):
- source = self.repo.sources_by_uri['ldapuser']
- eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0]
- self.sexecute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': eid})
- self.commit()
- source.reset_caches()
- rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
- 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},
- '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.synchronize()
- rset = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})
- self.assertEqual(len(rset), 1)
-
def test_nonregr1(self):
self.sexecute('Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, E owned_by X, '
'X modification_date AA',
@@ -403,7 +503,6 @@
'OR (EXISTS(U in_group H, ME in_group H, NOT H name "users")), U login UL, U is CWUser)',
{'x': self.session.user.eid})
-
class GlobTrFuncTC(TestCase):
def test_count(self):
@@ -438,6 +537,7 @@
res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]])
self.assertEqual(res, [[1, 5], [2, 4], [3, 6]])
+
class RQL2LDAPFilterTC(RQLGeneratorTC):
tags = RQLGeneratorTC.tags | Tags(('ldap'))
--- a/server/test/unittest_msplanner.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/unittest_msplanner.py Tue Jul 10 15:07:52 2012 +0200
@@ -914,14 +914,16 @@
('FetchStep', [('Any X WHERE X is CWUser', [{'X': 'CWUser'}])],
[self.ldap, self.system], None, {'X': 'table2.C0'}, []),
('UnionFetchStep', [
- ('FetchStep', [('Any X WHERE EXISTS(X owned_by %s), X is Basket' % ueid, [{'X': 'Basket'}])],
+ ('FetchStep', [('Any X WHERE EXISTS(%s use_email X), X is EmailAddress' % ueid,
+ [{'X': 'EmailAddress'}]),
+ ('Any X WHERE EXISTS(X owned_by %s), X is Basket' % ueid, [{'X': 'Basket'}])],
[self.system], {}, {'X': 'table0.C0'}, []),
('UnionFetchStep',
[('FetchStep', [('Any X WHERE X is IN(Card, Note, State)',
[{'X': 'Card'}, {'X': 'Note'}, {'X': 'State'}])],
[self.cards, self.system], {}, {'X': 'table0.C0'}, []),
('FetchStep',
- [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Old, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
+ [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, Old, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
[{'X': 'BaseTransition'}, {'X': 'Bookmark'},
{'X': 'CWAttribute'}, {'X': 'CWCache'},
{'X': 'CWConstraint'}, {'X': 'CWConstraintType'},
@@ -931,7 +933,7 @@
{'X': 'CWSource'},
{'X': 'CWUniqueTogetherConstraint'},
{'X': 'Comment'}, {'X': 'Division'},
- {'X': 'Email'}, {'X': 'EmailAddress'},
+ {'X': 'Email'},
{'X': 'EmailPart'}, {'X': 'EmailThread'},
{'X': 'ExternalUri'}, {'X': 'File'},
{'X': 'Folder'}, {'X': 'Old'},
@@ -972,7 +974,8 @@
('FetchStep', [('Any X WHERE X is CWUser', [{'X': 'CWUser'}])],
[self.ldap, self.system], None, {'X': 'table3.C0'}, []),
('UnionFetchStep',
- [('FetchStep', [('Any ET,X WHERE X is ET, EXISTS(X owned_by %s), ET is CWEType, X is Basket' % ueid,
+ [('FetchStep', [('Any ET,X WHERE X is ET, EXISTS(%s use_email X), ET is CWEType, X is EmailAddress' % ueid,
+ [{'ET': 'CWEType', 'X': 'EmailAddress'}]), ('Any ET,X WHERE X is ET, EXISTS(X owned_by %s), ET is CWEType, X is Basket' % ueid,
[{'ET': 'CWEType', 'X': 'Basket'}])],
[self.system], {}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []),
('FetchStep', [('Any ET,X WHERE X is ET, (EXISTS(X owned_by %(ueid)s)) OR ((((EXISTS(D concerne C?, C owned_by %(ueid)s, C type "X", X identity D, C is Division, D is Affaire)) OR (EXISTS(H concerne G?, G owned_by %(ueid)s, G type "X", X identity H, G is SubDivision, H is Affaire))) OR (EXISTS(I concerne F?, F owned_by %(ueid)s, F type "X", X identity I, F is Societe, I is Affaire))) OR (EXISTS(J concerne E?, E owned_by %(ueid)s, X identity J, E is Note, J is Affaire))), ET is CWEType, X is Affaire' % {'ueid': ueid},
@@ -987,7 +990,7 @@
[self.system], {'X': 'table3.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []),
# extra UnionFetchStep could be avoided but has no cost, so don't care
('UnionFetchStep',
- [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Old, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
+ [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, Old, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
[{'X': 'BaseTransition', 'ET': 'CWEType'},
{'X': 'Bookmark', 'ET': 'CWEType'}, {'X': 'CWAttribute', 'ET': 'CWEType'},
{'X': 'CWCache', 'ET': 'CWEType'}, {'X': 'CWConstraint', 'ET': 'CWEType'},
@@ -1000,7 +1003,7 @@
{'X': 'CWUniqueTogetherConstraint', 'ET': 'CWEType'},
{'X': 'Comment', 'ET': 'CWEType'},
{'X': 'Division', 'ET': 'CWEType'}, {'X': 'Email', 'ET': 'CWEType'},
- {'X': 'EmailAddress', 'ET': 'CWEType'}, {'X': 'EmailPart', 'ET': 'CWEType'},
+ {'X': 'EmailPart', 'ET': 'CWEType'},
{'X': 'EmailThread', 'ET': 'CWEType'}, {'X': 'ExternalUri', 'ET': 'CWEType'},
{'X': 'File', 'ET': 'CWEType'}, {'X': 'Folder', 'ET': 'CWEType'},
{'X': 'Old', 'ET': 'CWEType'}, {'X': 'Personne', 'ET': 'CWEType'},
--- a/server/test/unittest_postgres.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/unittest_postgres.py Tue Jul 10 15:07:52 2012 +0200
@@ -25,7 +25,7 @@
from cubicweb.devtools import ApptestConfiguration
from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.entities.adapters import IFTIndexableAdapter
AT_LOGILAB = socket.gethostname().endswith('.logilab.fr') # XXX
@@ -50,7 +50,7 @@
content=u'cubicweb cubicweb')
self.commit()
self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
- [[c1.eid], [c3.eid], [c2.eid]])
+ [(c1.eid,), (c3.eid,), (c2.eid,)])
def test_attr_weight(self):
@@ -67,7 +67,7 @@
content=u'autre chose')
self.commit()
self.assertEqual(req.execute('Card X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
- [[c3.eid], [c1.eid], [c2.eid]])
+ [(c3.eid,), (c1.eid,), (c2.eid,)])
def test_entity_weight(self):
class PersonneIFTIndexableAdapter(IFTIndexableAdapter):
@@ -80,7 +80,7 @@
c3 = req.create_entity('Comment', content=u'cubicweb cubicweb cubicweb', comments=c1)
self.commit()
self.assertEqual(req.execute('Any X ORDERBY FTIRANK(X) DESC WHERE X has_text "cubicweb"').rows,
- [[c1.eid], [c3.eid], [c2.eid]])
+ [(c1.eid,), (c3.eid,), (c2.eid,)])
def test_tz_datetime(self):
--- a/server/test/unittest_querier.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/unittest_querier.py Tue Jul 10 15:07:52 2012 +0200
@@ -122,7 +122,7 @@
self.assertEqual(len(union.children), 1)
self.assertEqual(len(union.children[0].with_), 1)
subq = union.children[0].with_[0].query
- self.assertEqual(len(subq.children), 3)
+ self.assertEqual(len(subq.children), 4)
self.assertEqual([t.as_string() for t in union.children[0].selection],
['ETN','COUNT(X)'])
self.assertEqual([t.as_string() for t in union.children[0].groupby],
@@ -147,7 +147,7 @@
'X': 'Affaire',
'ET': 'CWEType', 'ETN': 'String'}])
rql, solutions = partrqls[1]
- self.assertEqual(rql, 'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Note, Old, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
+ self.assertEqual(rql, 'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, Note, Old, Personne, RQLExpression, Societe, State, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)')
self.assertListEqual(sorted(solutions),
sorted([{'X': 'BaseTransition', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'Bookmark', 'ETN': 'String', 'ET': 'CWEType'},
@@ -168,7 +168,6 @@
{'X': 'CWUniqueTogetherConstraint', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'CWUser', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'Email', 'ETN': 'String', 'ET': 'CWEType'},
- {'X': 'EmailAddress', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'EmailPart', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'EmailThread', 'ETN': 'String', 'ET': 'CWEType'},
{'X': 'ExternalUri', 'ETN': 'String', 'ET': 'CWEType'},
@@ -189,12 +188,14 @@
{'X': 'WorkflowTransition', 'ETN': 'String', 'ET': 'CWEType'}]))
rql, solutions = partrqls[2]
self.assertEqual(rql,
+ 'Any ETN,X WHERE X is ET, ET name ETN, EXISTS(%(D)s use_email X), '
+ 'ET is CWEType, X is EmailAddress')
+ self.assertEqual(solutions, [{'X': 'EmailAddress', 'ET': 'CWEType', 'ETN': 'String'}])
+ rql, solutions = partrqls[3]
+ self.assertEqual(rql,
'Any ETN,X WHERE X is ET, ET name ETN, EXISTS(X owned_by %(C)s), '
'ET is CWEType, X is Basket')
- self.assertEqual(solutions, [{'ET': 'CWEType',
- 'X': 'Basket',
- 'ETN': 'String',
- }])
+ self.assertEqual(solutions, [{'X': 'Basket', 'ET': 'CWEType', 'ETN': 'String'}])
def test_preprocess_security_aggregat(self):
plan = self._prepare_plan('Any MAX(X)')
@@ -204,7 +205,7 @@
self.assertEqual(len(union.children), 1)
self.assertEqual(len(union.children[0].with_), 1)
subq = union.children[0].with_[0].query
- self.assertEqual(len(subq.children), 3)
+ self.assertEqual(len(subq.children), 4)
self.assertEqual([t.as_string() for t in union.children[0].selection],
['MAX(X)'])
@@ -1118,13 +1119,12 @@
(using cachekey on sql generation returned always the same query for an eid,
whatever the relation)
"""
- s = self.user_groups_session('users')
- aeid, = self.o.execute(s, 'INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"')[0]
+ aeid, = self.execute('INSERT EmailAddress X: X address "toto@logilab.fr", X alias "hop"')[0]
# XXX would be nice if the rql below was enough...
#'INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y'
- eeid, = self.o.execute(s, 'INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y WHERE Y is EmailAddress')[0]
- self.o.execute(s, "DELETE Email X")
- sqlc = s.cnxset['system']
+ eeid, = self.execute('INSERT Email X: X messageid "<1234>", X subject "test", X sender Y, X recipients Y WHERE Y is EmailAddress')[0]
+ self.execute("DELETE Email X")
+ sqlc = self.session.cnxset['system']
sqlc.execute('SELECT * FROM recipients_relation')
self.assertEqual(len(sqlc.fetchall()), 0)
sqlc.execute('SELECT * FROM owned_by_relation WHERE eid_from=%s'%eeid)
--- a/server/test/unittest_repository.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/unittest_repository.py Tue Jul 10 15:07:52 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.
@@ -34,9 +34,9 @@
from cubicweb import (BadConnectionId, RepositoryError, ValidationError,
UnknownEid, AuthenticationError, Unauthorized, QueryError)
-from cubicweb.selectors import is_instance
+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
@@ -379,6 +379,66 @@
# 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(self.repo.config.appid, u'admin', password=u'gingkow',
+ host='tcp://127.0.0.1:41415',
+ 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)
--- a/server/test/unittest_rql2sql.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/unittest_rql2sql.py Tue Jul 10 15:07:52 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_security.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/unittest_security.py Tue Jul 10 15:07:52 2012 +0200
@@ -590,5 +590,13 @@
self.execute, 'SET TI to_state S WHERE TI eid %(ti)s, S name "pitetre"',
{'ti': trinfo.eid})
+ def test_emailaddress_security(self):
+ self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
+ self.execute('INSERT EmailAddress X: X address "anon", U use_email X WHERE U login "anon"').get_entity(0, 0)
+ self.commit()
+ self.assertEqual(len(self.execute('Any X WHERE X is EmailAddress')), 2)
+ self.login('anon')
+ self.assertEqual(len(self.execute('Any X WHERE X is EmailAddress')), 1)
+
if __name__ == '__main__':
unittest_main()
--- a/server/test/unittest_storage.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/unittest_storage.py Tue Jul 10 15:07:52 2012 +0200
@@ -22,12 +22,13 @@
from logilab.common.testlib import unittest_main, tag, Tags
from cubicweb.devtools.testlib import CubicWebTC
+import os
import os.path as osp
import shutil
import tempfile
from cubicweb import Binary, QueryError
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.server.sources import storages
from cubicweb.server.hook import Hook, Operation
@@ -90,6 +91,8 @@
expected_filepath = osp.join(self.tempdir, '%s_data_%s' %
(f1.eid, f1.data_name))
self.assertTrue(osp.isfile(expected_filepath))
+ # file should be read only
+ self.assertFalse(os.access(expected_filepath, os.W_OK))
self.assertEqual(file(expected_filepath).read(), 'the-data')
self.rollback()
self.assertFalse(osp.isfile(expected_filepath))
--- a/server/test/unittest_undo.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/test/unittest_undo.py Tue Jul 10 15:07:52 2012 +0200
@@ -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"'))
@@ -193,14 +205,12 @@
c2 = session.create_entity('Card', title=u'hip', content=u'hip')
p.set_relations(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
@@ -213,10 +223,9 @@
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()
self.assertEqual(cm.exception.entity, self.toto.eid)
@@ -229,9 +238,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'))
@@ -288,12 +296,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.set_relations(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.set_relations(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.set_relations(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.set_relations(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.set_relations(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.set_relations(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.set_attributes(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.set_attributes(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)
--- a/server/utils.py Tue Jul 10 10:33:19 2012 +0200
+++ b/server/utils.py Tue Jul 10 15:07:52 2012 +0200
@@ -52,7 +52,7 @@
return md5crypt(secret, self.salt.encode('ascii')).decode('utf-8')
_calc_checksum = calc_checksum
-_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt'])
+_CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'])
def crypt_password(passwd, salt=None):
"""return the encrypted password using the given salt or a generated one
@@ -139,12 +139,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
@@ -157,7 +157,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)
@@ -203,3 +203,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/debian/DISTNAME.prerm.tmpl Tue Jul 10 10:33:19 2012 +0200
+++ /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 Tue Jul 10 10:33:19 2012 +0200
+++ b/skeleton/debian/compat Tue Jul 10 15:07:52 2012 +0200
@@ -1,1 +1,1 @@
-5
+7
--- a/skeleton/debian/control.tmpl Tue Jul 10 10:33:19 2012 +0200
+++ b/skeleton/debian/control.tmpl Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/skeleton/debian/copyright.tmpl Tue Jul 10 15:07:52 2012 +0200
@@ -1,4 +1,4 @@
-Upstream Author:
+Upstream Author:
%(author)s <%(author-email)s>
--- a/skeleton/debian/rules.tmpl Tue Jul 10 10:33:19 2012 +0200
+++ b/skeleton/debian/rules.tmpl Tue Jul 10 15:07:52 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/sobjects/__init__.py Tue Jul 10 10:33:19 2012 +0200
+++ b/sobjects/__init__.py Tue Jul 10 15:07:52 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,6 +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/>.
-"""server side objects
+"""server side objects"""
+
+import os.path as osp
-"""
+def registration_callback(vreg):
+ vreg.register_all(globals().values(), __name__)
+ global URL_MAPPING
+ URL_MAPPING = {}
+ if vreg.config.apphome:
+ url_mapping_file = osp.join(vreg.config.apphome, 'urlmapping.py')
+ if osp.exists(url_mapping_file):
+ URL_MAPPING = eval(file(url_mapping_file).read())
+ vreg.info('using url mapping %s from %s', URL_MAPPING, url_mapping_file)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/cwxmlparser.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,502 @@
+# 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.
+#
+# 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/>.
+"""datafeed parser for xml generated by cubicweb
+
+Example of mapping for CWEntityXMLParser::
+
+ {u'CWUser': { # EntityType
+ (u'in_group', u'subject', u'link'): [ # (rtype, role, action)
+ (u'CWGroup', {u'linkattr': u'name'})], # -> rules = [(EntityType, options), ...]
+ (u'tags', u'object', u'link-or-create'): [ # (...)
+ (u'Tag', {u'linkattr': u'name'})], # -> ...
+ (u'use_email', u'subject', u'copy'): [ # (...)
+ (u'EmailAddress', {})] # -> ...
+ }
+ }
+
+"""
+
+from datetime import datetime, timedelta, time
+from urllib import urlencode
+from cgi import parse_qs # in urlparse with python >= 2.6
+
+from logilab.common.date import todate, totime
+from logilab.common.textutils import splitstrip, text_to_dict
+from logilab.common.decorators import classproperty
+
+from yams.constraints import BASE_CONVERTERS
+from yams.schema import role_name as rn
+
+from cubicweb import ValidationError, RegistryException, typed_eid
+from cubicweb.view import Component
+from cubicweb.server.sources import datafeed
+from cubicweb.server.hook import match_rtype
+
+# XXX see cubicweb.cwvreg.YAMS_TO_PY
+# XXX see cubicweb.web.views.xmlrss.SERIALIZERS
+DEFAULT_CONVERTERS = BASE_CONVERTERS.copy()
+DEFAULT_CONVERTERS['String'] = unicode
+DEFAULT_CONVERTERS['Password'] = lambda x: x.encode('utf8')
+def convert_date(ustr):
+ return todate(datetime.strptime(ustr, '%Y-%m-%d'))
+DEFAULT_CONVERTERS['Date'] = convert_date
+def convert_datetime(ustr):
+ if '.' in ustr: # assume %Y-%m-%d %H:%M:%S.mmmmmm
+ ustr = ustr.split('.',1)[0]
+ return datetime.strptime(ustr, '%Y-%m-%d %H:%M:%S')
+DEFAULT_CONVERTERS['Datetime'] = convert_datetime
+# XXX handle timezone, though this will be enough as TZDatetime are
+# serialized without time zone by default (UTC time). See
+# cw.web.views.xmlrss.SERIALIZERS.
+DEFAULT_CONVERTERS['TZDatetime'] = convert_datetime
+def convert_time(ustr):
+ return totime(datetime.strptime(ustr, '%H:%M:%S'))
+DEFAULT_CONVERTERS['Time'] = convert_time
+DEFAULT_CONVERTERS['TZTime'] = convert_time
+def convert_interval(ustr):
+ return time(seconds=int(ustr))
+DEFAULT_CONVERTERS['Interval'] = convert_interval
+
+def extract_typed_attrs(eschema, stringdict, converters=DEFAULT_CONVERTERS):
+ typeddict = {}
+ for rschema in eschema.subject_relations():
+ if rschema.final and rschema in stringdict:
+ if rschema in ('eid', 'cwuri', 'cwtype', 'cwsource'):
+ continue
+ attrtype = eschema.destination(rschema)
+ value = stringdict[rschema]
+ if value is not None:
+ value = converters[attrtype](value)
+ typeddict[rschema.type] = value
+ return typeddict
+
+def rtype_role_rql(rtype, role):
+ if role == 'object':
+ return 'Y %s X WHERE X eid %%(x)s' % rtype
+ else:
+ return 'X %s Y WHERE X eid %%(x)s' % rtype
+
+
+class CWEntityXMLParser(datafeed.DataFeedXMLParser):
+ """datafeed parser for the 'xml' entity view
+
+ Most of the logic is delegated to the following components:
+
+ * an "item builder" component, turning an etree xml node into a specific
+ python dictionary representing an entity
+
+ * "action" components, selected given an entity, a relation and its role in
+ the relation, and responsible to link the entity to given related items
+ (eg dictionary)
+
+ So the parser is only doing the gluing service and the connection to the
+ source.
+ """
+ __regid__ = 'cw.entityxml'
+
+ def __init__(self, *args, **kwargs):
+ super(CWEntityXMLParser, self).__init__(*args, **kwargs)
+ self._parsed_urls = {}
+ self._processed_entities = set()
+
+ def select_linker(self, action, rtype, role, entity=None):
+ try:
+ return self._cw.vreg['components'].select(
+ 'cw.entityxml.action.%s' % action, self._cw, entity=entity,
+ rtype=rtype, role=role, parser=self)
+ except RegistryException:
+ raise RegistryException('Unknown action %s' % action)
+
+ def list_actions(self):
+ reg = self._cw.vreg['components']
+ return sorted(clss[0].action for rid, clss in reg.iteritems()
+ if rid.startswith('cw.entityxml.action.'))
+
+ # mapping handling #########################################################
+
+ def add_schema_config(self, schemacfg, checkonly=False):
+ """added CWSourceSchemaConfig, modify mapping accordingly"""
+ _ = self._cw._
+ try:
+ rtype = schemacfg.schema.rtype.name
+ except AttributeError:
+ msg = _("entity and relation types can't be mapped, only attributes "
+ "or relations")
+ raise ValidationError(schemacfg.eid, {rn('cw_for_schema', 'subject'): msg})
+ if schemacfg.options:
+ options = text_to_dict(schemacfg.options)
+ else:
+ options = {}
+ try:
+ role = options.pop('role')
+ if role not in ('subject', 'object'):
+ raise KeyError
+ except KeyError:
+ msg = _('"role=subject" or "role=object" must be specified in options')
+ raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
+ try:
+ action = options.pop('action')
+ linker = self.select_linker(action, rtype, role)
+ linker.check_options(options, schemacfg.eid)
+ except KeyError:
+ msg = _('"action" must be specified in options; allowed values are '
+ '%s') % ', '.join(self.list_actions())
+ raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
+ except RegistryException:
+ msg = _('allowed values for "action" are %s') % ', '.join(self.list_actions())
+ raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
+ if not checkonly:
+ if role == 'subject':
+ etype = schemacfg.schema.stype.name
+ ttype = schemacfg.schema.otype.name
+ else:
+ etype = schemacfg.schema.otype.name
+ ttype = schemacfg.schema.stype.name
+ etyperules = self.source.mapping.setdefault(etype, {})
+ etyperules.setdefault((rtype, role, action), []).append(
+ (ttype, options) )
+ self.source.mapping_idx[schemacfg.eid] = (
+ etype, rtype, role, action, ttype)
+
+ def del_schema_config(self, schemacfg, checkonly=False):
+ """deleted CWSourceSchemaConfig, modify mapping accordingly"""
+ etype, rtype, role, action, ttype = self.source.mapping_idx[schemacfg.eid]
+ rules = self.source.mapping[etype][(rtype, role, action)]
+ rules = [x for x in rules if not x[0] == ttype]
+ if not rules:
+ del self.source.mapping[etype][(rtype, role, action)]
+
+ # import handling ##########################################################
+
+ 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)
+
+ def parse_etree(self, parent):
+ for node in list(parent):
+ builder = self._cw.vreg['components'].select(
+ 'cw.entityxml.item-builder', self._cw, node=node,
+ parser=self)
+ yield builder.build_item()
+
+ def process_item(self, item, rels):
+ """
+ item and rels are what's returned by the item builder `build_item` method:
+
+ * `item` is an {attribute: value} dictionary
+ * `rels` is for relations and structured as
+ {role: {relation: [(related item, related rels)...]}
+ """
+ entity = self.extid2entity(str(item['cwuri']), item['cwtype'],
+ cwsource=item['cwsource'], item=item)
+ if entity is None:
+ return None
+ if entity.eid in self._processed_entities:
+ return entity
+ self._processed_entities.add(entity.eid)
+ if not (self.created_during_pull(entity) or self.updated_during_pull(entity)):
+ attrs = extract_typed_attrs(entity.e_schema, item)
+ self.update_if_necessary(entity, attrs)
+ self.process_relations(entity, rels)
+ return entity
+
+ def process_relations(self, entity, rels):
+ etype = entity.__regid__
+ for (rtype, role, action), rules in self.source.mapping.get(etype, {}).iteritems():
+ try:
+ related_items = rels[role][rtype]
+ except KeyError:
+ self.import_log.record_error('relation %s-%s not found in xml export of %s'
+ % (rtype, role, etype))
+ continue
+ try:
+ linker = self.select_linker(action, rtype, role, entity)
+ except RegistryException:
+ self.import_log.record_error('no linker for action %s' % action)
+ else:
+ linker.link_items(related_items, rules)
+
+ def before_entity_copy(self, entity, sourceparams):
+ """IDataFeedParser callback"""
+ attrs = extract_typed_attrs(entity.e_schema, sourceparams['item'])
+ entity.cw_edited.update(attrs)
+
+
+ def normalize_url(self, url):
+ """overriden to add vid=xml"""
+ url = super(CWEntityXMLParser, self).normalize_url(url)
+ if url.startswith('http'):
+ try:
+ url, qs = url.split('?', 1)
+ except ValueError:
+ params = {}
+ else:
+ params = parse_qs(qs)
+ if not 'vid' in params:
+ params['vid'] = ['xml']
+ return url + '?' + self._cw.build_url_params(**params)
+ return url
+
+ def complete_url(self, url, etype=None, known_relations=None):
+ """append to the url's query string information about relation that should
+ be included in the resulting xml, according to source mapping.
+
+ If etype is not specified, try to guess it using the last path part of
+ the url, i.e. the format used by default in cubicweb to map all entities
+ of a given type as in 'http://mysite.org/EntityType'.
+
+ If `known_relations` is given, it should be a dictionary of already
+ known relations, so they don't get queried again.
+ """
+ try:
+ url, qs = url.split('?', 1)
+ except ValueError:
+ qs = ''
+ # XXX vid will be added by later call to normalize_url (in parent class)
+ params = parse_qs(qs)
+ if etype is None:
+ try:
+ etype = url.rsplit('/', 1)[1]
+ except ValueError:
+ return url + '?' + self._cw.build_url_params(**params)
+ try:
+ etype = self._cw.vreg.case_insensitive_etypes[etype.lower()]
+ except KeyError:
+ return url + '?' + self._cw.build_url_params(**params)
+ relations = params.setdefault('relation', [])
+ for rtype, role, _ in self.source.mapping.get(etype, ()):
+ if known_relations and rtype in known_relations.get('role', ()):
+ continue
+ reldef = '%s-%s' % (rtype, role)
+ if not reldef in relations:
+ relations.append(reldef)
+ return url + '?' + self._cw.build_url_params(**params)
+
+ def complete_item(self, item, rels):
+ try:
+ return self._parsed_urls[item['cwuri']]
+ except KeyError:
+ itemurl = self.complete_url(item['cwuri'], item['cwtype'], rels)
+ item_rels = list(self.parse(itemurl))
+ assert len(item_rels) == 1, 'url %s expected to bring back one '\
+ 'and only one entity, got %s' % (itemurl, len(item_rels))
+ self._parsed_urls[item['cwuri']] = item_rels[0]
+ if rels:
+ # XXX (do it better) merge relations
+ new_rels = item_rels[0][1]
+ new_rels.get('subject', {}).update(rels.get('subject', {}))
+ new_rels.get('object', {}).update(rels.get('object', {}))
+ return item_rels[0]
+
+
+class CWEntityXMLItemBuilder(Component):
+ __regid__ = 'cw.entityxml.item-builder'
+
+ def __init__(self, _cw, parser, node, **kwargs):
+ super(CWEntityXMLItemBuilder, self).__init__(_cw, **kwargs)
+ self.parser = parser
+ self.node = node
+
+ def build_item(self):
+ """parse a XML document node and return two dictionaries defining (part
+ of) an entity:
+
+ - {attribute: value}
+ - {role: {relation: [(related item, related rels)...]}
+ """
+ node = self.node
+ item = dict(node.attrib.items())
+ item['cwtype'] = unicode(node.tag)
+ item.setdefault('cwsource', None)
+ try:
+ item['eid'] = typed_eid(item['eid'])
+ except KeyError:
+ # cw < 3.11 compat mode XXX
+ item['eid'] = typed_eid(node.find('eid').text)
+ item['cwuri'] = node.find('cwuri').text
+ rels = {}
+ for child in node:
+ role = child.get('role')
+ if role:
+ # relation
+ related = rels.setdefault(role, {}).setdefault(child.tag, [])
+ related += self.parser.parse_etree(child)
+ elif child.text:
+ # attribute
+ item[child.tag] = unicode(child.text)
+ else:
+ # None attribute (empty tag)
+ item[child.tag] = None
+ return item, rels
+
+
+class CWEntityXMLActionCopy(Component):
+ """implementation of cubicweb entity xml parser's'copy' action
+
+ Takes no option.
+ """
+ __regid__ = 'cw.entityxml.action.copy'
+
+ def __init__(self, _cw, parser, rtype, role, entity=None, **kwargs):
+ super(CWEntityXMLActionCopy, self).__init__(_cw, **kwargs)
+ self.parser = parser
+ self.rtype = rtype
+ self.role = role
+ self.entity = entity
+
+ @classproperty
+ def action(cls):
+ return cls.__regid__.rsplit('.', 1)[-1]
+
+ def check_options(self, options, eid):
+ self._check_no_options(options, eid)
+
+ def _check_no_options(self, options, eid, msg=None):
+ if options:
+ if msg is None:
+ msg = self._cw._("'%s' action doesn't take any options") % self.action
+ raise ValidationError(eid, {rn('options', 'subject'): msg})
+
+ def link_items(self, others, rules):
+ assert not any(x[1] for x in rules), "'copy' action takes no option"
+ ttypes = frozenset([x[0] for x in rules])
+ eids = [] # local eids
+ for item, rels in others:
+ if item['cwtype'] in ttypes:
+ item, rels = self.parser.complete_item(item, rels)
+ other_entity = self.parser.process_item(item, rels)
+ if other_entity is not None:
+ eids.append(other_entity.eid)
+ if eids:
+ self._set_relation(eids)
+ else:
+ self._clear_relation(ttypes)
+
+ def _clear_relation(self, ttypes):
+ if not self.parser.created_during_pull(self.entity):
+ if len(ttypes) > 1:
+ typerestr = ', Y is IN(%s)' % ','.join(ttypes)
+ else:
+ typerestr = ', Y is %s' % ','.join(ttypes)
+ self._cw.execute('DELETE ' + rtype_role_rql(self.rtype, self.role) + typerestr,
+ {'x': self.entity.eid})
+
+ def _set_relation(self, eids):
+ assert eids
+ rtype = self.rtype
+ rqlbase = rtype_role_rql(rtype, self.role)
+ eidstr = ','.join(str(eid) for eid in eids)
+ self._cw.execute('DELETE %s, NOT Y eid IN (%s)' % (rqlbase, eidstr),
+ {'x': self.entity.eid})
+ if self.role == 'object':
+ rql = 'SET %s, Y eid IN (%s), NOT Y %s X' % (rqlbase, eidstr, rtype)
+ else:
+ rql = 'SET %s, Y eid IN (%s), NOT X %s Y' % (rqlbase, eidstr, rtype)
+ self._cw.execute(rql, {'x': self.entity.eid})
+
+
+class CWEntityXMLActionLink(CWEntityXMLActionCopy):
+ """implementation of cubicweb entity xml parser's'link' action
+
+ requires a 'linkattr' option to control search of the linked entity.
+ """
+ __regid__ = 'cw.entityxml.action.link'
+
+ def check_options(self, options, eid):
+ if not 'linkattr' in options:
+ msg = self._cw._("'%s' action requires 'linkattr' option") % self.action
+ raise ValidationError(eid, {rn('options', 'subject'): msg})
+
+ create_when_not_found = False
+
+ def link_items(self, others, rules):
+ for ttype, options in rules:
+ searchattrs = splitstrip(options.get('linkattr', ''))
+ self._related_link(ttype, others, searchattrs)
+
+ def _related_link(self, ttype, others, searchattrs):
+ def issubset(x,y):
+ return all(z in y for z in x)
+ eids = [] # local eids
+ log = self.parser.import_log
+ for item, rels in others:
+ if item['cwtype'] != ttype:
+ continue
+ if not issubset(searchattrs, item):
+ item, rels = self.parser.complete_item(item, rels)
+ if not issubset(searchattrs, item):
+ log.record_error('missing attribute, got %s expected keys %s'
+ % (item, searchattrs))
+ continue
+ # XXX str() needed with python < 2.6
+ kwargs = dict((str(attr), item[attr]) for attr in searchattrs)
+ targets = self._find_entities(item, kwargs)
+ if len(targets) == 1:
+ entity = targets[0]
+ elif not targets and self.create_when_not_found:
+ entity = self._cw.create_entity(item['cwtype'], **kwargs)
+ else:
+ if len(targets) > 1:
+ log.record_error('ambiguous link: found %s entity %s with attributes %s'
+ % (len(targets), item['cwtype'], kwargs))
+ else:
+ log.record_error('can not find %s entity with attributes %s'
+ % (item['cwtype'], kwargs))
+ continue
+ eids.append(entity.eid)
+ self.parser.process_relations(entity, rels)
+ if eids:
+ self._set_relation(eids)
+ else:
+ self._clear_relation((ttype,))
+
+ def _find_entities(self, item, kwargs):
+ return tuple(self._cw.find_entities(item['cwtype'], **kwargs))
+
+
+class CWEntityXMLActionLinkInState(CWEntityXMLActionLink):
+ """custom implementation of cubicweb entity xml parser's'link' action for
+ in_state relation
+ """
+ __select__ = match_rtype('in_state')
+
+ def check_options(self, options, eid):
+ super(CWEntityXMLActionLinkInState, self).check_options(options, eid)
+ if not 'name' in options['linkattr']:
+ msg = self._cw._("'%s' action for in_state relation should at least have 'linkattr=name' option") % self.action
+ raise ValidationError(eid, {rn('options', 'subject'): msg})
+
+ def _find_entities(self, item, kwargs):
+ assert 'name' in item # XXX else, complete_item
+ state_name = item['name']
+ wf = self.entity.cw_adapt_to('IWorkflowable').current_workflow
+ state = wf.state_by_name(state_name)
+ if state is None:
+ return ()
+ return (state,)
+
+
+class CWEntityXMLActionLinkOrCreate(CWEntityXMLActionLink):
+ """implementation of cubicweb entity xml parser's'link-or-create' action
+
+ requires a 'linkattr' option to control search of the linked entity.
+ """
+ __regid__ = 'cw.entityxml.action.link-or-create'
+ create_when_not_found = True
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/ldapparser.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,152 @@
+# 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.
+#
+# 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/>.
+"""cubicweb ldap feed source
+
+unlike ldapuser source, this source is copy based and will import ldap content
+(beside passwords for authentication) into the system source.
+"""
+
+from logilab.common.decorators import cached
+from logilab.common.shellutils import generate_password
+
+from cubicweb import Binary
+from cubicweb.server.utils import crypt_password
+from cubicweb.server.sources import datafeed
+
+
+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):
+ """IDataFeedParser main entry point"""
+ source = self.source
+ searchstr = '(&%s)' % ''.join(source.base_filters)
+ 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 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):
+ 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.set_attributes(**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]
+ return tdict
+
+ def before_entity_copy(self, entity, sourceparams):
+ if entity.__regid__ == 'EmailAddress':
+ 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(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)
+ self._process_email(entity, sourceparams)
+
+ def is_deleted(self, extid, etype, eid):
+ try:
+ extid, _ = extid.rsplit('@@', 1)
+ except ValueError:
+ pass
+ return not self.source.object_exists_in_ldap(extid)
+
+ def _process_email(self, entity, userdict):
+ try:
+ emailaddrs = userdict[self.source.user_rev_attrs['email']]
+ except KeyError:
+ return # no email for that user, nothing to do
+ if not isinstance(emailaddrs, list):
+ emailaddrs = [emailaddrs]
+ for emailaddr in emailaddrs:
+ # search for existant email first, may be coming from another source
+ rset = self._cw.execute('EmailAddress X WHERE X address %(addr)s',
+ {'addr': emailaddr})
+ if not rset:
+ # not found, create it. first forge an external id
+ emailextid = userdict['dn'] + '@@' + emailaddr
+ email = self.extid2entity(emailextid, 'EmailAddress',
+ address=emailaddr)
+ if entity.primary_email:
+ entity.set_relations(use_email=email)
+ else:
+ entity.set_relations(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
+ def _get_group(self, name):
+ return self._cw.execute('Any X WHERE X is CWGroup, X name %(name)s',
+ {'name': name}).get_entity(0, 0)
--- a/sobjects/notification.py Tue Jul 10 10:33:19 2012 +0200
+++ b/sobjects/notification.py Tue Jul 10 15:07:52 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,8 +24,8 @@
from logilab.common.textutils import normalize_text
from logilab.common.deprecation import class_renamed, class_moved, deprecated
+from logilab.common.registry import yes
-from cubicweb.selectors import yes
from cubicweb.view import Component
from cubicweb.mail import NotificationView as BaseNotificationView, SkipEmail
from cubicweb.server.hook import SendMailOp
--- a/sobjects/parsers.py Tue Jul 10 10:33:19 2012 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,508 +0,0 @@
-# copyright 2010-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/>.
-"""datafeed parser for xml generated by cubicweb
-
-Example of mapping for CWEntityXMLParser::
-
- {u'CWUser': { # EntityType
- (u'in_group', u'subject', u'link'): [ # (rtype, role, action)
- (u'CWGroup', {u'linkattr': u'name'})], # -> rules = [(EntityType, options), ...]
- (u'tags', u'object', u'link-or-create'): [ # (...)
- (u'Tag', {u'linkattr': u'name'})], # -> ...
- (u'use_email', u'subject', u'copy'): [ # (...)
- (u'EmailAddress', {})] # -> ...
- }
- }
-
-"""
-
-import os.path as osp
-from datetime import datetime, timedelta, time
-from urllib import urlencode
-from cgi import parse_qs # in urlparse with python >= 2.6
-
-from logilab.common.date import todate, totime
-from logilab.common.textutils import splitstrip, text_to_dict
-from logilab.common.decorators import classproperty
-
-from yams.constraints import BASE_CONVERTERS
-from yams.schema import role_name as rn
-
-from cubicweb import ValidationError, RegistryException, typed_eid
-from cubicweb.view import Component
-from cubicweb.server.sources import datafeed
-from cubicweb.server.hook import match_rtype
-
-# XXX see cubicweb.cwvreg.YAMS_TO_PY
-# XXX see cubicweb.web.views.xmlrss.SERIALIZERS
-DEFAULT_CONVERTERS = BASE_CONVERTERS.copy()
-DEFAULT_CONVERTERS['String'] = unicode
-DEFAULT_CONVERTERS['Password'] = lambda x: x.encode('utf8')
-def convert_date(ustr):
- return todate(datetime.strptime(ustr, '%Y-%m-%d'))
-DEFAULT_CONVERTERS['Date'] = convert_date
-def convert_datetime(ustr):
- if '.' in ustr: # assume %Y-%m-%d %H:%M:%S.mmmmmm
- ustr = ustr.split('.',1)[0]
- return datetime.strptime(ustr, '%Y-%m-%d %H:%M:%S')
-DEFAULT_CONVERTERS['Datetime'] = convert_datetime
-# XXX handle timezone, though this will be enough as TZDatetime are
-# serialized without time zone by default (UTC time). See
-# cw.web.views.xmlrss.SERIALIZERS.
-DEFAULT_CONVERTERS['TZDatetime'] = convert_datetime
-def convert_time(ustr):
- return totime(datetime.strptime(ustr, '%H:%M:%S'))
-DEFAULT_CONVERTERS['Time'] = convert_time
-DEFAULT_CONVERTERS['TZTime'] = convert_time
-def convert_interval(ustr):
- return time(seconds=int(ustr))
-DEFAULT_CONVERTERS['Interval'] = convert_interval
-
-def extract_typed_attrs(eschema, stringdict, converters=DEFAULT_CONVERTERS):
- typeddict = {}
- for rschema in eschema.subject_relations():
- if rschema.final and rschema in stringdict:
- if rschema in ('eid', 'cwuri', 'cwtype', 'cwsource'):
- continue
- attrtype = eschema.destination(rschema)
- value = stringdict[rschema]
- if value is not None:
- value = converters[attrtype](value)
- typeddict[rschema.type] = value
- return typeddict
-
-def rtype_role_rql(rtype, role):
- if role == 'object':
- return 'Y %s X WHERE X eid %%(x)s' % rtype
- else:
- return 'X %s Y WHERE X eid %%(x)s' % rtype
-
-
-class CWEntityXMLParser(datafeed.DataFeedXMLParser):
- """datafeed parser for the 'xml' entity view
-
- Most of the logic is delegated to the following components:
-
- * an "item builder" component, turning an etree xml node into a specific
- python dictionnary representing an entity
-
- * "action" components, selected given an entity, a relation and its role in
- the relation, and responsible to link the entity to given related items
- (eg dictionnary)
-
- So the parser is only doing the gluing service and the connection to the
- source.
- """
- __regid__ = 'cw.entityxml'
-
- def __init__(self, *args, **kwargs):
- super(CWEntityXMLParser, self).__init__(*args, **kwargs)
- self._parsed_urls = {}
- self._processed_entities = set()
-
- def select_linker(self, action, rtype, role, entity=None):
- try:
- return self._cw.vreg['components'].select(
- 'cw.entityxml.action.%s' % action, self._cw, entity=entity,
- rtype=rtype, role=role, parser=self)
- except RegistryException:
- raise RegistryException('Unknown action %s' % action)
-
- def list_actions(self):
- reg = self._cw.vreg['components']
- return sorted(clss[0].action for rid, clss in reg.iteritems()
- if rid.startswith('cw.entityxml.action.'))
-
- # mapping handling #########################################################
-
- def add_schema_config(self, schemacfg, checkonly=False):
- """added CWSourceSchemaConfig, modify mapping accordingly"""
- _ = self._cw._
- try:
- rtype = schemacfg.schema.rtype.name
- except AttributeError:
- msg = _("entity and relation types can't be mapped, only attributes "
- "or relations")
- raise ValidationError(schemacfg.eid, {rn('cw_for_schema', 'subject'): msg})
- if schemacfg.options:
- options = text_to_dict(schemacfg.options)
- else:
- options = {}
- try:
- role = options.pop('role')
- if role not in ('subject', 'object'):
- raise KeyError
- except KeyError:
- msg = _('"role=subject" or "role=object" must be specified in options')
- raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
- try:
- action = options.pop('action')
- linker = self.select_linker(action, rtype, role)
- linker.check_options(options, schemacfg.eid)
- except KeyError:
- msg = _('"action" must be specified in options; allowed values are '
- '%s') % ', '.join(self.list_actions())
- raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
- except RegistryException:
- msg = _('allowed values for "action" are %s') % ', '.join(self.list_actions())
- raise ValidationError(schemacfg.eid, {rn('options', 'subject'): msg})
- if not checkonly:
- if role == 'subject':
- etype = schemacfg.schema.stype.name
- ttype = schemacfg.schema.otype.name
- else:
- etype = schemacfg.schema.otype.name
- ttype = schemacfg.schema.stype.name
- etyperules = self.source.mapping.setdefault(etype, {})
- etyperules.setdefault((rtype, role, action), []).append(
- (ttype, options) )
- self.source.mapping_idx[schemacfg.eid] = (
- etype, rtype, role, action, ttype)
-
- def del_schema_config(self, schemacfg, checkonly=False):
- """deleted CWSourceSchemaConfig, modify mapping accordingly"""
- etype, rtype, role, action, ttype = self.source.mapping_idx[schemacfg.eid]
- rules = self.source.mapping[etype][(rtype, role, action)]
- rules = [x for x in rules if not x[0] == ttype]
- if not rules:
- del self.source.mapping[etype][(rtype, role, action)]
-
- # import handling ##########################################################
-
- def process(self, url, raise_on_error=False, partialcommit=True):
- """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)
-
- def parse_etree(self, parent):
- for node in list(parent):
- builder = self._cw.vreg['components'].select(
- 'cw.entityxml.item-builder', self._cw, node=node,
- parser=self)
- yield builder.build_item()
-
- def process_item(self, item, rels):
- """
- item and rels are what's returned by the item builder `build_item` method:
-
- * `item` is an {attribute: value} dictionary
- * `rels` is for relations and structured as
- {role: {relation: [(related item, related rels)...]}
- """
- entity = self.extid2entity(str(item['cwuri']), item['cwtype'],
- cwsource=item['cwsource'], item=item)
- if entity is None:
- return None
- if entity.eid in self._processed_entities:
- return entity
- self._processed_entities.add(entity.eid)
- if not (self.created_during_pull(entity) or self.updated_during_pull(entity)):
- self.notify_updated(entity)
- attrs = extract_typed_attrs(entity.e_schema, item)
- # check modification date and compare attribute values to only
- # update what's actually needed
- entity.complete(tuple(attrs))
- 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)
- self.process_relations(entity, rels)
- return entity
-
- def process_relations(self, entity, rels):
- etype = entity.__regid__
- for (rtype, role, action), rules in self.source.mapping.get(etype, {}).iteritems():
- try:
- related_items = rels[role][rtype]
- except KeyError:
- self.import_log.record_error('relation %s-%s not found in xml export of %s'
- % (rtype, role, etype))
- continue
- try:
- linker = self.select_linker(action, rtype, role, entity)
- except RegistryException:
- self.import_log.record_error('no linker for action %s' % action)
- else:
- linker.link_items(related_items, rules)
-
- def before_entity_copy(self, entity, sourceparams):
- """IDataFeedParser callback"""
- attrs = extract_typed_attrs(entity.e_schema, sourceparams['item'])
- entity.cw_edited.update(attrs)
-
- def complete_url(self, url, etype=None, known_relations=None):
- """append to the url's query string information about relation that should
- be included in the resulting xml, according to source mapping.
-
- If etype is not specified, try to guess it using the last path part of
- the url, i.e. the format used by default in cubicweb to map all entities
- of a given type as in 'http://mysite.org/EntityType'.
-
- If `known_relations` is given, it should be a dictionary of already
- known relations, so they don't get queried again.
- """
- try:
- url, qs = url.split('?', 1)
- except ValueError:
- qs = ''
- params = parse_qs(qs)
- if not 'vid' in params:
- params['vid'] = ['xml']
- if etype is None:
- try:
- etype = url.rsplit('/', 1)[1]
- except ValueError:
- return url + '?' + self._cw.build_url_params(**params)
- try:
- etype = self._cw.vreg.case_insensitive_etypes[etype.lower()]
- except KeyError:
- return url + '?' + self._cw.build_url_params(**params)
- relations = params.setdefault('relation', [])
- for rtype, role, _ in self.source.mapping.get(etype, ()):
- if known_relations and rtype in known_relations.get('role', ()):
- continue
- reldef = '%s-%s' % (rtype, role)
- if not reldef in relations:
- relations.append(reldef)
- return url + '?' + self._cw.build_url_params(**params)
-
- def complete_item(self, item, rels):
- try:
- return self._parsed_urls[item['cwuri']]
- except KeyError:
- itemurl = self.complete_url(item['cwuri'], item['cwtype'], rels)
- item_rels = list(self.parse(itemurl))
- assert len(item_rels) == 1, 'url %s expected to bring back one '\
- 'and only one entity, got %s' % (itemurl, len(item_rels))
- self._parsed_urls[item['cwuri']] = item_rels[0]
- if rels:
- # XXX (do it better) merge relations
- new_rels = item_rels[0][1]
- new_rels.get('subject', {}).update(rels.get('subject', {}))
- new_rels.get('object', {}).update(rels.get('object', {}))
- return item_rels[0]
-
-
-class CWEntityXMLItemBuilder(Component):
- __regid__ = 'cw.entityxml.item-builder'
-
- def __init__(self, _cw, parser, node, **kwargs):
- super(CWEntityXMLItemBuilder, self).__init__(_cw, **kwargs)
- self.parser = parser
- self.node = node
-
- def build_item(self):
- """parse a XML document node and return two dictionaries defining (part
- of) an entity:
-
- - {attribute: value}
- - {role: {relation: [(related item, related rels)...]}
- """
- node = self.node
- item = dict(node.attrib.items())
- item['cwtype'] = unicode(node.tag)
- item.setdefault('cwsource', None)
- try:
- item['eid'] = typed_eid(item['eid'])
- except KeyError:
- # cw < 3.11 compat mode XXX
- item['eid'] = typed_eid(node.find('eid').text)
- item['cwuri'] = node.find('cwuri').text
- rels = {}
- for child in node:
- role = child.get('role')
- if role:
- # relation
- related = rels.setdefault(role, {}).setdefault(child.tag, [])
- related += self.parser.parse_etree(child)
- elif child.text:
- # attribute
- item[child.tag] = unicode(child.text)
- else:
- # None attribute (empty tag)
- item[child.tag] = None
- return item, rels
-
-
-class CWEntityXMLActionCopy(Component):
- """implementation of cubicweb entity xml parser's'copy' action
-
- Takes no option.
- """
- __regid__ = 'cw.entityxml.action.copy'
-
- def __init__(self, _cw, parser, rtype, role, entity=None, **kwargs):
- super(CWEntityXMLActionCopy, self).__init__(_cw, **kwargs)
- self.parser = parser
- self.rtype = rtype
- self.role = role
- self.entity = entity
-
- @classproperty
- def action(cls):
- return cls.__regid__.rsplit('.', 1)[-1]
-
- def check_options(self, options, eid):
- self._check_no_options(options, eid)
-
- def _check_no_options(self, options, eid, msg=None):
- if options:
- if msg is None:
- msg = self._cw._("'%s' action doesn't take any options") % self.action
- raise ValidationError(eid, {rn('options', 'subject'): msg})
-
- def link_items(self, others, rules):
- assert not any(x[1] for x in rules), "'copy' action takes no option"
- ttypes = frozenset([x[0] for x in rules])
- eids = [] # local eids
- for item, rels in others:
- if item['cwtype'] in ttypes:
- item, rels = self.parser.complete_item(item, rels)
- other_entity = self.parser.process_item(item, rels)
- if other_entity is not None:
- eids.append(other_entity.eid)
- if eids:
- self._set_relation(eids)
- else:
- self._clear_relation(ttypes)
-
- def _clear_relation(self, ttypes):
- if not self.parser.created_during_pull(self.entity):
- if len(ttypes) > 1:
- typerestr = ', Y is IN(%s)' % ','.join(ttypes)
- else:
- typerestr = ', Y is %s' % ','.join(ttypes)
- self._cw.execute('DELETE ' + rtype_role_rql(self.rtype, self.role) + typerestr,
- {'x': self.entity.eid})
-
- def _set_relation(self, eids):
- assert eids
- rtype = self.rtype
- rqlbase = rtype_role_rql(rtype, self.role)
- eidstr = ','.join(str(eid) for eid in eids)
- self._cw.execute('DELETE %s, NOT Y eid IN (%s)' % (rqlbase, eidstr),
- {'x': self.entity.eid})
- if self.role == 'object':
- rql = 'SET %s, Y eid IN (%s), NOT Y %s X' % (rqlbase, eidstr, rtype)
- else:
- rql = 'SET %s, Y eid IN (%s), NOT X %s Y' % (rqlbase, eidstr, rtype)
- self._cw.execute(rql, {'x': self.entity.eid})
-
-
-class CWEntityXMLActionLink(CWEntityXMLActionCopy):
- """implementation of cubicweb entity xml parser's'link' action
-
- requires a 'linkattr' option to control search of the linked entity.
- """
- __regid__ = 'cw.entityxml.action.link'
-
- def check_options(self, options, eid):
- if not 'linkattr' in options:
- msg = self._cw._("'%s' action requires 'linkattr' option") % self.action
- raise ValidationError(eid, {rn('options', 'subject'): msg})
-
- create_when_not_found = False
-
- def link_items(self, others, rules):
- for ttype, options in rules:
- searchattrs = splitstrip(options.get('linkattr', ''))
- self._related_link(ttype, others, searchattrs)
-
- def _related_link(self, ttype, others, searchattrs):
- def issubset(x,y):
- return all(z in y for z in x)
- eids = [] # local eids
- log = self.parser.import_log
- for item, rels in others:
- if item['cwtype'] != ttype:
- continue
- if not issubset(searchattrs, item):
- item, rels = self.parser.complete_item(item, rels)
- if not issubset(searchattrs, item):
- log.record_error('missing attribute, got %s expected keys %s'
- % (item, searchattrs))
- continue
- # XXX str() needed with python < 2.6
- kwargs = dict((str(attr), item[attr]) for attr in searchattrs)
- targets = self._find_entities(item, kwargs)
- if len(targets) == 1:
- entity = targets[0]
- elif not targets and self.create_when_not_found:
- entity = self._cw.create_entity(item['cwtype'], **kwargs)
- else:
- if len(targets) > 1:
- log.record_error('ambiguous link: found %s entity %s with attributes %s'
- % (len(targets), item['cwtype'], kwargs))
- else:
- log.record_error('can not find %s entity with attributes %s'
- % (item['cwtype'], kwargs))
- continue
- eids.append(entity.eid)
- self.parser.process_relations(entity, rels)
- if eids:
- self._set_relation(eids)
- else:
- self._clear_relation((ttype,))
-
- def _find_entities(self, item, kwargs):
- return tuple(self._cw.find_entities(item['cwtype'], **kwargs))
-
-
-class CWEntityXMLActionLinkInState(CWEntityXMLActionLink):
- """custom implementation of cubicweb entity xml parser's'link' action for
- in_state relation
- """
- __select__ = match_rtype('in_state')
-
- def check_options(self, options, eid):
- super(CWEntityXMLActionLinkInState, self).check_options(options, eid)
- if not 'name' in options['linkattr']:
- msg = self._cw._("'%s' action for in_state relation should at least have 'linkattr=name' option") % self.action
- raise ValidationError(eid, {rn('options', 'subject'): msg})
-
- def _find_entities(self, item, kwargs):
- assert 'name' in item # XXX else, complete_item
- state_name = item['name']
- wf = self.entity.cw_adapt_to('IWorkflowable').current_workflow
- state = wf.state_by_name(state_name)
- if state is None:
- return ()
- return (state,)
-
-
-class CWEntityXMLActionLinkOrCreate(CWEntityXMLActionLink):
- """implementation of cubicweb entity xml parser's'link-or-create' action
-
- requires a 'linkattr' option to control search of the linked entity.
- """
- __regid__ = 'cw.entityxml.action.link-or-create'
- create_when_not_found = True
-
-
-def registration_callback(vreg):
- vreg.register_all(globals().values(), __name__)
- global URL_MAPPING
- URL_MAPPING = {}
- if vreg.config.apphome:
- url_mapping_file = osp.join(vreg.config.apphome, 'urlmapping.py')
- if osp.exists(url_mapping_file):
- URL_MAPPING = eval(file(url_mapping_file).read())
- vreg.info('using url mapping %s from %s', URL_MAPPING, url_mapping_file)
--- a/sobjects/supervising.py Tue Jul 10 10:33:19 2012 +0200
+++ b/sobjects/supervising.py Tue Jul 10 15:07:52 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,7 @@
_ = unicode
from cubicweb import UnknownEid
-from cubicweb.selectors import none_rset
+from cubicweb.predicates import none_rset
from cubicweb.schema import display_name
from cubicweb.view import Component
from cubicweb.mail import format_mail
--- a/sobjects/test/data/sobjects/__init__.py Tue Jul 10 10:33:19 2012 +0200
+++ b/sobjects/test/data/sobjects/__init__.py Tue Jul 10 15:07:52 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,7 +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/>.
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.sobjects.notification import StatusChangeMixIn, NotificationView
class UserStatusChangeView(StatusChangeMixIn, NotificationView):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/test/unittest_cwxmlparser.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,329 @@
+# 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.
+#
+# 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 __future__ import with_statement
+
+from datetime import datetime
+
+from cubicweb.devtools.testlib import CubicWebTC
+
+from cubicweb.sobjects.cwxmlparser import CWEntityXMLParser
+
+orig_parse = CWEntityXMLParser.parse
+
+def parse(self, url):
+ try:
+ url = RELATEDXML[url.split('?')[0]]
+ except KeyError:
+ pass
+ return orig_parse(self, url)
+
+def setUpModule():
+ CWEntityXMLParser.parse = parse
+
+def tearDownModule():
+ CWEntityXMLParser.parse = orig_parse
+
+
+BASEXML = ''.join(u'''
+<rset size="1">
+ <CWUser eid="5" cwuri="http://pouet.org/5" cwsource="system">
+ <login>sthenault</login>
+ <upassword>toto</upassword>
+ <last_login_time>2011-01-25 14:14:06</last_login_time>
+ <creation_date>2010-01-22 10:27:59</creation_date>
+ <modification_date>2011-01-25 14:14:06</modification_date>
+ <use_email role="subject">
+ <EmailAddress cwuri="http://pouet.org/6" eid="6"/>
+ </use_email>
+ <in_group role="subject">
+ <CWGroup cwuri="http://pouet.org/7" eid="7"/>
+ <CWGroup cwuri="http://pouet.org/8" eid="8"/>
+ </in_group>
+ <tags role="object">
+ <Tag cwuri="http://pouet.org/9" eid="9"/>
+ <Tag cwuri="http://pouet.org/10" eid="10"/>
+ </tags>
+ <in_state role="subject">
+ <State cwuri="http://pouet.org/11" eid="11" name="activated"/>
+ </in_state>
+ </CWUser>
+</rset>
+'''.splitlines())
+
+RELATEDXML = {
+ 'http://pouet.org/6': u'''
+<rset size="1">
+ <EmailAddress eid="6" cwuri="http://pouet.org/6">
+ <address>syt@logilab.fr</address>
+ <modification_date>2010-04-13 14:35:56</modification_date>
+ <creation_date>2010-04-13 14:35:56</creation_date>
+ <tags role="object">
+ <Tag cwuri="http://pouet.org/9" eid="9"/>
+ </tags>
+ </EmailAddress>
+</rset>
+''',
+ 'http://pouet.org/7': u'''
+<rset size="1">
+ <CWGroup eid="7" cwuri="http://pouet.org/7">
+ <name>users</name>
+ <tags role="object">
+ <Tag cwuri="http://pouet.org/9" eid="9"/>
+ </tags>
+ </CWGroup>
+</rset>
+''',
+ 'http://pouet.org/8': u'''
+<rset size="1">
+ <CWGroup eid="8" cwuri="http://pouet.org/8">
+ <name>unknown</name>
+ </CWGroup>
+</rset>
+''',
+ 'http://pouet.org/9': u'''
+<rset size="1">
+ <Tag eid="9" cwuri="http://pouet.org/9">
+ <name>hop</name>
+ </Tag>
+</rset>
+''',
+ 'http://pouet.org/10': u'''
+<rset size="1">
+ <Tag eid="10" cwuri="http://pouet.org/10">
+ <name>unknown</name>
+ </Tag>
+</rset>
+''',
+ }
+
+
+OTHERXML = ''.join(u'''
+<rset size="1">
+ <CWUser eid="5" cwuri="http://pouet.org/5" cwsource="myfeed">
+ <login>sthenault</login>
+ <upassword>toto</upassword>
+ <last_login_time>2011-01-25 14:14:06</last_login_time>
+ <creation_date>2010-01-22 10:27:59</creation_date>
+ <modification_date>2011-01-25 14:14:06</modification_date>
+ <in_group role="subject">
+ <CWGroup cwuri="http://pouet.org/7" eid="7"/>
+ </in_group>
+ </CWUser>
+</rset>
+'''.splitlines()
+)
+
+
+class CWEntityXMLParserTC(CubicWebTC):
+ """/!\ this test use a pre-setup database /!\, if you modify above xml,
+ REMOVE THE DATABASE TEMPLATE else it won't be considered
+ """
+ test_db_id = 'xmlparser'
+ @classmethod
+ def pre_setup_database(cls, session, config):
+ myfeed = session.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
+ parser=u'cw.entityxml', url=BASEXML)
+ myotherfeed = session.create_entity('CWSource', name=u'myotherfeed', type=u'datafeed',
+ parser=u'cw.entityxml', url=OTHERXML)
+ session.commit()
+ myfeed.init_mapping([(('CWUser', 'use_email', '*'),
+ u'role=subject\naction=copy'),
+ (('CWUser', 'in_group', '*'),
+ u'role=subject\naction=link\nlinkattr=name'),
+ (('CWUser', 'in_state', '*'),
+ u'role=subject\naction=link\nlinkattr=name'),
+ (('*', 'tags', '*'),
+ u'role=object\naction=link-or-create\nlinkattr=name'),
+ ])
+ myotherfeed.init_mapping([(('CWUser', 'in_group', '*'),
+ u'role=subject\naction=link\nlinkattr=name'),
+ (('CWUser', 'in_state', '*'),
+ u'role=subject\naction=link\nlinkattr=name'),
+ ])
+ session.create_entity('Tag', name=u'hop')
+
+ def test_complete_url(self):
+ dfsource = self.repo.sources_by_uri['myfeed']
+ parser = dfsource._get_parser(self.session)
+ self.assertEqual(parser.complete_url('http://www.cubicweb.org/CWUser'),
+ 'http://www.cubicweb.org/CWUser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
+ self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser'),
+ 'http://www.cubicweb.org/cwuser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject')
+ self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser?vid=rdf&relation=hop'),
+ 'http://www.cubicweb.org/cwuser?relation=hop&relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=rdf')
+ self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&vid=rdf&relation=hop'),
+ 'http://www.cubicweb.org/?rql=cwuser&relation=hop&vid=rdf')
+ self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&relation=hop'),
+ 'http://www.cubicweb.org/?rql=cwuser&relation=hop')
+
+
+ def test_actions(self):
+ dfsource = self.repo.sources_by_uri['myfeed']
+ self.assertEqual(dfsource.mapping,
+ {u'CWUser': {
+ (u'in_group', u'subject', u'link'): [
+ (u'CWGroup', {u'linkattr': u'name'})],
+ (u'in_state', u'subject', u'link'): [
+ (u'State', {u'linkattr': u'name'})],
+ (u'tags', u'object', u'link-or-create'): [
+ (u'Tag', {u'linkattr': u'name'})],
+ (u'use_email', u'subject', u'copy'): [
+ (u'EmailAddress', {})]
+ },
+ u'CWGroup': {
+ (u'tags', u'object', u'link-or-create'): [
+ (u'Tag', {u'linkattr': u'name'})],
+ },
+ u'EmailAddress': {
+ (u'tags', u'object', u'link-or-create'): [
+ (u'Tag', {u'linkattr': u'name'})],
+ },
+ })
+ session = self.repo.internal_session(safe=True)
+ stats = dfsource.pull_data(session, force=True, raise_on_error=True)
+ self.assertEqual(sorted(stats.keys()), ['checked', 'created', 'updated'])
+ self.assertEqual(len(stats['created']), 2)
+ self.assertEqual(stats['updated'], set())
+
+ user = self.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
+ self.assertEqual(user.creation_date, datetime(2010, 01, 22, 10, 27, 59))
+ self.assertEqual(user.modification_date, datetime(2011, 01, 25, 14, 14, 06))
+ self.assertEqual(user.cwuri, 'http://pouet.org/5')
+ self.assertEqual(user.cw_source[0].name, 'myfeed')
+ self.assertEqual(user.absolute_url(), 'http://pouet.org/5')
+ self.assertEqual(len(user.use_email), 1)
+ # copy action
+ email = user.use_email[0]
+ self.assertEqual(email.address, 'syt@logilab.fr')
+ self.assertEqual(email.cwuri, 'http://pouet.org/6')
+ self.assertEqual(email.absolute_url(), 'http://pouet.org/6')
+ self.assertEqual(email.cw_source[0].name, 'myfeed')
+ self.assertEqual(len(email.reverse_tags), 1)
+ self.assertEqual(email.reverse_tags[0].name, 'hop')
+ # link action
+ self.assertFalse(self.execute('CWGroup X WHERE X name "unknown"'))
+ groups = sorted([g.name for g in user.in_group])
+ self.assertEqual(groups, ['users'])
+ group = user.in_group[0]
+ self.assertEqual(len(group.reverse_tags), 1)
+ self.assertEqual(group.reverse_tags[0].name, 'hop')
+ # link or create action
+ tags = set([(t.name, t.cwuri.replace(str(t.eid), ''), t.cw_source[0].name)
+ for t in user.reverse_tags])
+ self.assertEqual(tags, set((('hop', 'http://testing.fr/cubicweb/', 'system'),
+ ('unknown', 'http://testing.fr/cubicweb/', 'system')))
+ )
+ 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']), 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']), 0)
+ self.assertEqual(len(stats['checked']), 2)
+ session.commit()
+
+ # test move to system source
+ self.sexecute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': email.eid})
+ self.commit()
+ rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
+ self.assertEqual(len(rset), 1)
+ e = rset.get_entity(0, 0)
+ self.assertEqual(e.eid, email.eid)
+ self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system',
+ 'use-cwuri-as-url': False},
+ 'type': 'EmailAddress',
+ 'extid': None})
+ self.assertEqual(e.cw_source[0].name, 'system')
+ self.assertEqual(e.reverse_use_email[0].login, 'sthenault')
+ self.commit()
+ # test everything is still fine after source synchronization
+ 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)
+ rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
+ self.assertEqual(len(rset), 1)
+ e = rset.get_entity(0, 0)
+ self.assertEqual(e.eid, email.eid)
+ self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system',
+ 'use-cwuri-as-url': False},
+ 'type': 'EmailAddress',
+ 'extid': None})
+ self.assertEqual(e.cw_source[0].name, 'system')
+ self.assertEqual(e.reverse_use_email[0].login, 'sthenault')
+ session.commit()
+
+ # test delete entity
+ e.cw_delete()
+ self.commit()
+ # test everything is still fine after source synchronization
+ 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)
+ rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
+ self.assertEqual(len(rset), 0)
+ rset = self.sexecute('Any X WHERE X use_email E, X login "sthenault"')
+ self.assertEqual(len(rset), 0)
+
+ def test_external_entity(self):
+ dfsource = self.repo.sources_by_uri['myotherfeed']
+ session = self.repo.internal_session(safe=True)
+ stats = dfsource.pull_data(session, force=True, raise_on_error=True)
+ user = self.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
+ self.assertEqual(user.creation_date, datetime(2010, 01, 22, 10, 27, 59))
+ self.assertEqual(user.modification_date, datetime(2011, 01, 25, 14, 14, 06))
+ self.assertEqual(user.cwuri, 'http://pouet.org/5')
+ self.assertEqual(user.cw_source[0].name, 'myfeed')
+
+ def test_noerror_missing_fti_attribute(self):
+ dfsource = self.repo.sources_by_uri['myfeed']
+ session = self.repo.internal_session(safe=True)
+ parser = dfsource._get_parser(session)
+ dfsource.process_urls(parser, ['''
+<rset size="1">
+ <Card eid="50" cwuri="http://pouet.org/50" cwsource="system">
+ <title>how-to</title>
+ </Card>
+</rset>
+'''], raise_on_error=True)
+
+ def test_noerror_unspecified_date(self):
+ dfsource = self.repo.sources_by_uri['myfeed']
+ session = self.repo.internal_session(safe=True)
+ parser = dfsource._get_parser(session)
+ dfsource.process_urls(parser, ['''
+<rset size="1">
+ <Card eid="50" cwuri="http://pouet.org/50" cwsource="system">
+ <title>how-to</title>
+ <content>how-to</content>
+ <synopsis>how-to</synopsis>
+ <creation_date/>
+ </Card>
+</rset>
+'''], raise_on_error=True)
+
+if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
+ unittest_main()
--- a/sobjects/test/unittest_parsers.py Tue Jul 10 10:33:19 2012 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,327 +0,0 @@
-# copyright 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/>.
-
-from __future__ import with_statement
-
-from datetime import datetime
-
-from cubicweb.devtools.testlib import CubicWebTC
-
-from cubicweb.sobjects.parsers import CWEntityXMLParser
-
-orig_parse = CWEntityXMLParser.parse
-
-def parse(self, url):
- try:
- url = RELATEDXML[url.split('?')[0]]
- except KeyError:
- pass
- return orig_parse(self, url)
-
-def setUpModule():
- CWEntityXMLParser.parse = parse
-
-def tearDownModule():
- CWEntityXMLParser.parse = orig_parse
-
-
-BASEXML = ''.join(u'''
-<rset size="1">
- <CWUser eid="5" cwuri="http://pouet.org/5" cwsource="system">
- <login>sthenault</login>
- <upassword>toto</upassword>
- <last_login_time>2011-01-25 14:14:06</last_login_time>
- <creation_date>2010-01-22 10:27:59</creation_date>
- <modification_date>2011-01-25 14:14:06</modification_date>
- <use_email role="subject">
- <EmailAddress cwuri="http://pouet.org/6" eid="6"/>
- </use_email>
- <in_group role="subject">
- <CWGroup cwuri="http://pouet.org/7" eid="7"/>
- <CWGroup cwuri="http://pouet.org/8" eid="8"/>
- </in_group>
- <tags role="object">
- <Tag cwuri="http://pouet.org/9" eid="9"/>
- <Tag cwuri="http://pouet.org/10" eid="10"/>
- </tags>
- <in_state role="subject">
- <State cwuri="http://pouet.org/11" eid="11" name="activated"/>
- </in_state>
- </CWUser>
-</rset>
-'''.splitlines())
-
-RELATEDXML = {
- 'http://pouet.org/6': u'''
-<rset size="1">
- <EmailAddress eid="6" cwuri="http://pouet.org/6">
- <address>syt@logilab.fr</address>
- <modification_date>2010-04-13 14:35:56</modification_date>
- <creation_date>2010-04-13 14:35:56</creation_date>
- <tags role="object">
- <Tag cwuri="http://pouet.org/9" eid="9"/>
- </tags>
- </EmailAddress>
-</rset>
-''',
- 'http://pouet.org/7': u'''
-<rset size="1">
- <CWGroup eid="7" cwuri="http://pouet.org/7">
- <name>users</name>
- <tags role="object">
- <Tag cwuri="http://pouet.org/9" eid="9"/>
- </tags>
- </CWGroup>
-</rset>
-''',
- 'http://pouet.org/8': u'''
-<rset size="1">
- <CWGroup eid="8" cwuri="http://pouet.org/8">
- <name>unknown</name>
- </CWGroup>
-</rset>
-''',
- 'http://pouet.org/9': u'''
-<rset size="1">
- <Tag eid="9" cwuri="http://pouet.org/9">
- <name>hop</name>
- </Tag>
-</rset>
-''',
- 'http://pouet.org/10': u'''
-<rset size="1">
- <Tag eid="10" cwuri="http://pouet.org/10">
- <name>unknown</name>
- </Tag>
-</rset>
-''',
- }
-
-
-OTHERXML = ''.join(u'''
-<rset size="1">
- <CWUser eid="5" cwuri="http://pouet.org/5" cwsource="myfeed">
- <login>sthenault</login>
- <upassword>toto</upassword>
- <last_login_time>2011-01-25 14:14:06</last_login_time>
- <creation_date>2010-01-22 10:27:59</creation_date>
- <modification_date>2011-01-25 14:14:06</modification_date>
- <in_group role="subject">
- <CWGroup cwuri="http://pouet.org/7" eid="7"/>
- </in_group>
- </CWUser>
-</rset>
-'''.splitlines()
-)
-
-
-class CWEntityXMLParserTC(CubicWebTC):
- """/!\ this test use a pre-setup database /!\, if you modify above xml,
- REMOVE THE DATABASE TEMPLATE else it won't be considered
- """
- test_db_id = 'xmlparser'
- @classmethod
- def pre_setup_database(cls, session, config):
- myfeed = session.create_entity('CWSource', name=u'myfeed', type=u'datafeed',
- parser=u'cw.entityxml', url=BASEXML)
- myotherfeed = session.create_entity('CWSource', name=u'myotherfeed', type=u'datafeed',
- parser=u'cw.entityxml', url=OTHERXML)
- session.commit()
- myfeed.init_mapping([(('CWUser', 'use_email', '*'),
- u'role=subject\naction=copy'),
- (('CWUser', 'in_group', '*'),
- u'role=subject\naction=link\nlinkattr=name'),
- (('CWUser', 'in_state', '*'),
- u'role=subject\naction=link\nlinkattr=name'),
- (('*', 'tags', '*'),
- u'role=object\naction=link-or-create\nlinkattr=name'),
- ])
- myotherfeed.init_mapping([(('CWUser', 'in_group', '*'),
- u'role=subject\naction=link\nlinkattr=name'),
- (('CWUser', 'in_state', '*'),
- u'role=subject\naction=link\nlinkattr=name'),
- ])
- session.create_entity('Tag', name=u'hop')
-
- def test_complete_url(self):
- dfsource = self.repo.sources_by_uri['myfeed']
- parser = dfsource._get_parser(self.session)
- self.assertEqual(parser.complete_url('http://www.cubicweb.org/CWUser'),
- 'http://www.cubicweb.org/CWUser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=xml')
- self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser'),
- 'http://www.cubicweb.org/cwuser?relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=xml')
- self.assertEqual(parser.complete_url('http://www.cubicweb.org/cwuser?vid=rdf&relation=hop'),
- 'http://www.cubicweb.org/cwuser?relation=hop&relation=tags-object&relation=in_group-subject&relation=in_state-subject&relation=use_email-subject&vid=rdf')
- self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&vid=rdf&relation=hop'),
- 'http://www.cubicweb.org/?rql=cwuser&relation=hop&vid=rdf')
- self.assertEqual(parser.complete_url('http://www.cubicweb.org/?rql=cwuser&relation=hop'),
- 'http://www.cubicweb.org/?rql=cwuser&relation=hop&vid=xml')
-
-
- def test_actions(self):
- dfsource = self.repo.sources_by_uri['myfeed']
- self.assertEqual(dfsource.mapping,
- {u'CWUser': {
- (u'in_group', u'subject', u'link'): [
- (u'CWGroup', {u'linkattr': u'name'})],
- (u'in_state', u'subject', u'link'): [
- (u'State', {u'linkattr': u'name'})],
- (u'tags', u'object', u'link-or-create'): [
- (u'Tag', {u'linkattr': u'name'})],
- (u'use_email', u'subject', u'copy'): [
- (u'EmailAddress', {})]
- },
- u'CWGroup': {
- (u'tags', u'object', u'link-or-create'): [
- (u'Tag', {u'linkattr': u'name'})],
- },
- u'EmailAddress': {
- (u'tags', u'object', u'link-or-create'): [
- (u'Tag', {u'linkattr': u'name'})],
- },
- })
- 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(len(stats['created']), 2)
- self.assertEqual(stats['updated'], set())
-
- user = self.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
- self.assertEqual(user.creation_date, datetime(2010, 01, 22, 10, 27, 59))
- self.assertEqual(user.modification_date, datetime(2011, 01, 25, 14, 14, 06))
- self.assertEqual(user.cwuri, 'http://pouet.org/5')
- self.assertEqual(user.cw_source[0].name, 'myfeed')
- self.assertEqual(user.absolute_url(), 'http://pouet.org/5')
- self.assertEqual(len(user.use_email), 1)
- # copy action
- email = user.use_email[0]
- self.assertEqual(email.address, 'syt@logilab.fr')
- self.assertEqual(email.cwuri, 'http://pouet.org/6')
- self.assertEqual(email.absolute_url(), 'http://pouet.org/6')
- self.assertEqual(email.cw_source[0].name, 'myfeed')
- self.assertEqual(len(email.reverse_tags), 1)
- self.assertEqual(email.reverse_tags[0].name, 'hop')
- # link action
- self.assertFalse(self.execute('CWGroup X WHERE X name "unknown"'))
- groups = sorted([g.name for g in user.in_group])
- self.assertEqual(groups, ['users'])
- group = user.in_group[0]
- self.assertEqual(len(group.reverse_tags), 1)
- self.assertEqual(group.reverse_tags[0].name, 'hop')
- # link or create action
- tags = set([(t.name, t.cwuri.replace(str(t.eid), ''), t.cw_source[0].name)
- for t in user.reverse_tags])
- self.assertEqual(tags, set((('hop', 'http://testing.fr/cubicweb/', 'system'),
- ('unknown', 'http://testing.fr/cubicweb/', 'system')))
- )
- 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.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)
- session.commit()
-
- # test move to system source
- self.sexecute('SET X cw_source S WHERE X eid %(x)s, S name "system"', {'x': email.eid})
- self.commit()
- rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
- self.assertEqual(len(rset), 1)
- e = rset.get_entity(0, 0)
- self.assertEqual(e.eid, email.eid)
- self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system',
- 'use-cwuri-as-url': False},
- 'type': 'EmailAddress',
- 'extid': None})
- self.assertEqual(e.cw_source[0].name, 'system')
- self.assertEqual(e.reverse_use_email[0].login, 'sthenault')
- self.commit()
- # test everything is still fine after source synchronization
- 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)
- rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
- self.assertEqual(len(rset), 1)
- e = rset.get_entity(0, 0)
- self.assertEqual(e.eid, email.eid)
- self.assertEqual(e.cw_metainformation(), {'source': {'type': u'native', 'uri': u'system',
- 'use-cwuri-as-url': False},
- 'type': 'EmailAddress',
- 'extid': None})
- self.assertEqual(e.cw_source[0].name, 'system')
- self.assertEqual(e.reverse_use_email[0].login, 'sthenault')
- session.commit()
-
- # test delete entity
- e.cw_delete()
- self.commit()
- # test everything is still fine after source synchronization
- 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)
- rset = self.sexecute('EmailAddress X WHERE X address "syt@logilab.fr"')
- self.assertEqual(len(rset), 0)
- rset = self.sexecute('Any X WHERE X use_email E, X login "sthenault"')
- self.assertEqual(len(rset), 0)
-
- def test_external_entity(self):
- dfsource = self.repo.sources_by_uri['myotherfeed']
- session = self.repo.internal_session(safe=True)
- stats = dfsource.pull_data(session, force=True, raise_on_error=True)
- user = self.execute('CWUser X WHERE X login "sthenault"').get_entity(0, 0)
- self.assertEqual(user.creation_date, datetime(2010, 01, 22, 10, 27, 59))
- self.assertEqual(user.modification_date, datetime(2011, 01, 25, 14, 14, 06))
- self.assertEqual(user.cwuri, 'http://pouet.org/5')
- self.assertEqual(user.cw_source[0].name, 'myfeed')
-
- def test_noerror_missing_fti_attribute(self):
- dfsource = self.repo.sources_by_uri['myfeed']
- session = self.repo.internal_session(safe=True)
- parser = dfsource._get_parser(session)
- dfsource.process_urls(parser, ['''
-<rset size="1">
- <Card eid="50" cwuri="http://pouet.org/50" cwsource="system">
- <title>how-to</title>
- </Card>
-</rset>
-'''], raise_on_error=True)
-
- def test_noerror_unspecified_date(self):
- dfsource = self.repo.sources_by_uri['myfeed']
- session = self.repo.internal_session(safe=True)
- parser = dfsource._get_parser(session)
- dfsource.process_urls(parser, ['''
-<rset size="1">
- <Card eid="50" cwuri="http://pouet.org/50" cwsource="system">
- <title>how-to</title>
- <content>how-to</content>
- <synopsis>how-to</synopsis>
- <creation_date/>
- </Card>
-</rset>
-'''], raise_on_error=True)
-
-if __name__ == '__main__':
- from logilab.common.testlib import unittest_main
- unittest_main()
--- a/test/data/views.py Tue Jul 10 10:33:19 2012 +0200
+++ b/test/data/views.py Tue Jul 10 15:07:52 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_dbapi.py Tue Jul 10 10:33:19 2012 +0200
+++ b/test/unittest_dbapi.py Tue Jul 10 15:07:52 2012 +0200
@@ -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 Tue Jul 10 10:33:19 2012 +0200
+++ b/test/unittest_entity.py Tue Jul 10 15:07:52 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.
@@ -19,7 +19,10 @@
"""unit tests for cubicweb.web.views.entities module"""
from datetime import datetime
+
from logilab.common import tempattr
+from logilab.common.decorators import clear_cache
+
from cubicweb import Binary, Unauthorized
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.mttransforms import HAS_TAL
@@ -316,12 +319,22 @@
'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, '
'S login AA, S firstname AB, S surname AC, S modification_date AD')
self.login('anon')
- email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
- rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
- self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
- 'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, '
- 'S login AA, S firstname AB, S surname AC, S modification_date AD, '
- 'AE eid %(AF)s, EXISTS(S identity AE, NOT AE in_group AG, AG name "guests", AG is CWGroup)')
+ rperms = self.schema['EmailAddress'].permissions['read']
+ clear_cache(self.schema['EmailAddress'], 'get_groups')
+ clear_cache(self.schema['EmailAddress'], 'get_rqlexprs')
+ self.schema['EmailAddress'].permissions['read'] = ('managers', 'users', 'guests',)
+ try:
+ email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
+ rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
+ self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
+ 'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, '
+ 'S login AA, S firstname AB, S surname AC, S modification_date AD, '
+ 'AE eid %(AF)s, EXISTS(S identity AE, NOT AE in_group AG, AG name "guests", AG is CWGroup)')
+ finally:
+ clear_cache(self.schema['EmailAddress'], 'get_groups')
+ clear_cache(self.schema['EmailAddress'], 'get_rqlexprs')
+ self.schema['EmailAddress'].permissions['read'] = rperms
+
def test_unrelated_rql_security_nonexistant(self):
self.login('anon')
@@ -459,31 +472,40 @@
1)
def test_unrelated_security(self):
- email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
- rset = email.unrelated('use_email', 'CWUser', 'object')
- self.assertEqual([x.login for x in rset.entities()], [u'admin', u'anon'])
- user = self.request().user
- rset = user.unrelated('use_email', 'EmailAddress', 'subject')
- self.assertEqual([x.address for x in rset.entities()], [u'hop'])
- req = self.request()
- self.create_user(req, 'toto')
- self.login('toto')
- email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
- rset = email.unrelated('use_email', 'CWUser', 'object')
- self.assertEqual([x.login for x in rset.entities()], ['toto'])
- user = self.request().user
- rset = user.unrelated('use_email', 'EmailAddress', 'subject')
- self.assertEqual([x.address for x in rset.entities()], ['hop'])
- user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0)
- rset = user.unrelated('use_email', 'EmailAddress', 'subject')
- self.assertEqual([x.address for x in rset.entities()], [])
- self.login('anon')
- email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
- rset = email.unrelated('use_email', 'CWUser', 'object')
- self.assertEqual([x.login for x in rset.entities()], [])
- user = self.request().user
- rset = user.unrelated('use_email', 'EmailAddress', 'subject')
- self.assertEqual([x.address for x in rset.entities()], [])
+ rperms = self.schema['EmailAddress'].permissions['read']
+ clear_cache(self.schema['EmailAddress'], 'get_groups')
+ clear_cache(self.schema['EmailAddress'], 'get_rqlexprs')
+ self.schema['EmailAddress'].permissions['read'] = ('managers', 'users', 'guests',)
+ try:
+ email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
+ rset = email.unrelated('use_email', 'CWUser', 'object')
+ self.assertEqual([x.login for x in rset.entities()], [u'admin', u'anon'])
+ user = self.request().user
+ rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+ self.assertEqual([x.address for x in rset.entities()], [u'hop'])
+ req = self.request()
+ self.create_user(req, 'toto')
+ self.login('toto')
+ email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
+ rset = email.unrelated('use_email', 'CWUser', 'object')
+ self.assertEqual([x.login for x in rset.entities()], ['toto'])
+ user = self.request().user
+ rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+ self.assertEqual([x.address for x in rset.entities()], ['hop'])
+ user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0)
+ rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+ self.assertEqual([x.address for x in rset.entities()], [])
+ self.login('anon')
+ email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
+ rset = email.unrelated('use_email', 'CWUser', 'object')
+ self.assertEqual([x.login for x in rset.entities()], [])
+ user = self.request().user
+ rset = user.unrelated('use_email', 'EmailAddress', 'subject')
+ self.assertEqual([x.address for x in rset.entities()], [])
+ finally:
+ clear_cache(self.schema['EmailAddress'], 'get_groups')
+ clear_cache(self.schema['EmailAddress'], 'get_rqlexprs')
+ self.schema['EmailAddress'].permissions['read'] = rperms
def test_unrelated_new_entity(self):
e = self.vreg['etypes'].etype_class('CWUser')(self.request())
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_predicates.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,342 @@
+# 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/>.
+"""unit tests for selectors mechanism"""
+from __future__ import with_statement
+
+from operator import eq, lt, le, gt
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb import Binary
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.predicates import (is_instance, adaptable, match_kwargs, match_user_groups,
+ multi_lines_rset, score_entity, is_in_state,
+ rql_condition, relation_possible)
+from cubicweb.selectors import on_transition # XXX on_transition is deprecated
+from cubicweb.web import action
+
+
+
+class ImplementsSelectorTC(CubicWebTC):
+ def test_etype_priority(self):
+ req = self.request()
+ f = req.create_entity('File', data_name=u'hop.txt', data=Binary('hop'))
+ rset = f.as_rset()
+ anyscore = is_instance('Any')(f.__class__, req, rset=rset)
+ idownscore = adaptable('IDownloadable')(f.__class__, req, rset=rset)
+ self.assertTrue(idownscore > anyscore, (idownscore, anyscore))
+ filescore = is_instance('File')(f.__class__, req, rset=rset)
+ self.assertTrue(filescore > idownscore, (filescore, idownscore))
+
+ def test_etype_inheritance_no_yams_inheritance(self):
+ cls = self.vreg['etypes'].etype_class('Personne')
+ self.assertFalse(is_instance('Societe').score_class(cls, self.request()))
+
+ def test_yams_inheritance(self):
+ cls = self.vreg['etypes'].etype_class('Transition')
+ self.assertEqual(is_instance('BaseTransition').score_class(cls, self.request()),
+ 3)
+
+ def test_outer_join(self):
+ req = self.request()
+ rset = req.execute('Any U,B WHERE B? bookmarked_by U, U login "anon"')
+ self.assertEqual(is_instance('Bookmark')(None, req, rset=rset, row=0, col=1),
+ 0)
+
+
+class WorkflowSelectorTC(CubicWebTC):
+ def _commit(self):
+ self.commit()
+ self.wf_entity.cw_clear_all_caches()
+
+ def setup_database(self):
+ wf = self.shell().add_workflow("wf_test", 'StateFull', default=True)
+ created = wf.add_state('created', initial=True)
+ validated = wf.add_state('validated')
+ abandoned = wf.add_state('abandoned')
+ wf.add_transition('validate', created, validated, ('managers',))
+ wf.add_transition('forsake', (created, validated,), abandoned, ('managers',))
+
+ def setUp(self):
+ super(WorkflowSelectorTC, self).setUp()
+ self.req = self.request()
+ self.wf_entity = self.req.create_entity('StateFull', name=u'')
+ self.rset = self.wf_entity.as_rset()
+ self.adapter = self.wf_entity.cw_adapt_to('IWorkflowable')
+ self._commit()
+ self.assertEqual(self.adapter.state, 'created')
+ # enable debug mode to state/transition validation on the fly
+ self.vreg.config.debugmode = True
+
+ def tearDown(self):
+ self.vreg.config.debugmode = False
+ super(WorkflowSelectorTC, self).tearDown()
+
+ def test_is_in_state(self):
+ for state in ('created', 'validated', 'abandoned'):
+ selector = is_in_state(state)
+ self.assertEqual(selector(None, self.req, rset=self.rset),
+ state=="created")
+
+ self.adapter.fire_transition('validate')
+ self._commit()
+ self.assertEqual(self.adapter.state, 'validated')
+
+ selector = is_in_state('created')
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+ selector = is_in_state('validated')
+ self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+ selector = is_in_state('validated', 'abandoned')
+ self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+ selector = is_in_state('abandoned')
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+
+ self.adapter.fire_transition('forsake')
+ self._commit()
+ self.assertEqual(self.adapter.state, 'abandoned')
+
+ selector = is_in_state('created')
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+ selector = is_in_state('validated')
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+ selector = is_in_state('validated', 'abandoned')
+ self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+ self.assertEqual(self.adapter.state, 'abandoned')
+ self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+
+ def test_is_in_state_unvalid_names(self):
+ selector = is_in_state("unknown")
+ with self.assertRaises(ValueError) as cm:
+ selector(None, self.req, rset=self.rset)
+ self.assertEqual(str(cm.exception),
+ "wf_test: unknown state(s): unknown")
+ selector = is_in_state("weird", "unknown", "created", "weird")
+ with self.assertRaises(ValueError) as cm:
+ selector(None, self.req, rset=self.rset)
+ self.assertEqual(str(cm.exception),
+ "wf_test: unknown state(s): unknown,weird")
+
+ def test_on_transition(self):
+ for transition in ('validate', 'forsake'):
+ selector = on_transition(transition)
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+
+ self.adapter.fire_transition('validate')
+ self._commit()
+ self.assertEqual(self.adapter.state, 'validated')
+
+ selector = on_transition("validate")
+ self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+ selector = on_transition("validate", "forsake")
+ self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+ selector = on_transition("forsake")
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+
+ self.adapter.fire_transition('forsake')
+ self._commit()
+ self.assertEqual(self.adapter.state, 'abandoned')
+
+ selector = on_transition("validate")
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+ selector = on_transition("validate", "forsake")
+ self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+ selector = on_transition("forsake")
+ self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+
+ def test_on_transition_unvalid_names(self):
+ selector = on_transition("unknown")
+ with self.assertRaises(ValueError) as cm:
+ selector(None, self.req, rset=self.rset)
+ self.assertEqual(str(cm.exception),
+ "wf_test: unknown transition(s): unknown")
+ selector = on_transition("weird", "unknown", "validate", "weird")
+ with self.assertRaises(ValueError) as cm:
+ selector(None, self.req, rset=self.rset)
+ self.assertEqual(str(cm.exception),
+ "wf_test: unknown transition(s): unknown,weird")
+
+ def test_on_transition_with_no_effect(self):
+ """selector will not be triggered with `change_state()`"""
+ self.adapter.change_state('validated')
+ self._commit()
+ self.assertEqual(self.adapter.state, 'validated')
+
+ selector = on_transition("validate")
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+ selector = on_transition("validate", "forsake")
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+ selector = on_transition("forsake")
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+
+
+class RelationPossibleTC(CubicWebTC):
+
+ def test_rqlst_1(self):
+ req = self.request()
+ selector = relation_possible('in_group')
+ select = self.vreg.parse(req, 'Any X WHERE X is CWUser').children[0]
+ score = selector(None, req, rset=1,
+ select=select, filtered_variable=select.defined_vars['X'])
+ self.assertEqual(score, 1)
+
+ def test_rqlst_2(self):
+ req = self.request()
+ selector = relation_possible('in_group')
+ select = self.vreg.parse(req, 'Any 1, COUNT(X) WHERE X is CWUser, X creation_date XD, '
+ 'Y creation_date YD, Y is CWGroup '
+ 'HAVING DAY(XD)=DAY(YD)').children[0]
+ score = selector(None, req, rset=1,
+ select=select, filtered_variable=select.defined_vars['X'])
+ self.assertEqual(score, 1)
+
+
+class MatchUserGroupsTC(CubicWebTC):
+ def test_owners_group(self):
+ """tests usage of 'owners' group with match_user_group"""
+ class SomeAction(action.Action):
+ __regid__ = 'yo'
+ category = 'foo'
+ __select__ = match_user_groups('owners')
+ self.vreg._loadedmods[__name__] = {}
+ self.vreg.register(SomeAction)
+ SomeAction.__registered__(self.vreg['actions'])
+ self.assertTrue(SomeAction in self.vreg['actions']['yo'], self.vreg['actions'])
+ try:
+ # login as a simple user
+ req = self.request()
+ self.create_user(req, 'john')
+ self.login('john')
+ # it should not be possible to use SomeAction not owned objects
+ req = self.request()
+ rset = req.execute('Any G WHERE G is CWGroup, G name "managers"')
+ self.assertFalse('yo' in dict(self.pactions(req, rset)))
+ # insert a new card, and check that we can use SomeAction on our object
+ self.execute('INSERT Card C: C title "zoubidou"')
+ self.commit()
+ req = self.request()
+ rset = req.execute('Card C WHERE C title "zoubidou"')
+ self.assertTrue('yo' in dict(self.pactions(req, rset)), self.pactions(req, rset))
+ # make sure even managers can't use the action
+ self.restore_connection()
+ req = self.request()
+ rset = req.execute('Card C WHERE C title "zoubidou"')
+ self.assertFalse('yo' in dict(self.pactions(req, rset)))
+ finally:
+ del self.vreg[SomeAction.__registry__][SomeAction.__regid__]
+
+
+class MultiLinesRsetSelectorTC(CubicWebTC):
+ def setUp(self):
+ super(MultiLinesRsetSelectorTC, self).setUp()
+ self.req = self.request()
+ self.req.execute('INSERT CWGroup G: G name "group1"')
+ self.req.execute('INSERT CWGroup G: G name "group2"')
+ self.commit()
+ self.rset = self.req.execute('Any G WHERE G is CWGroup')
+
+ def test_default_op_in_selector(self):
+ expected = len(self.rset)
+ selector = multi_lines_rset(expected)
+ self.assertEqual(selector(None, self.req, rset=self.rset), 1)
+ self.assertEqual(selector(None, self.req, None), 0)
+ selector = multi_lines_rset(expected + 1)
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+ self.assertEqual(selector(None, self.req, None), 0)
+ selector = multi_lines_rset(expected - 1)
+ self.assertEqual(selector(None, self.req, rset=self.rset), 0)
+ self.assertEqual(selector(None, self.req, None), 0)
+
+ def test_without_rset(self):
+ expected = len(self.rset)
+ selector = multi_lines_rset(expected)
+ self.assertEqual(selector(None, self.req, None), 0)
+ selector = multi_lines_rset(expected + 1)
+ self.assertEqual(selector(None, self.req, None), 0)
+ selector = multi_lines_rset(expected - 1)
+ self.assertEqual(selector(None, self.req, None), 0)
+
+ def test_with_operators(self):
+ expected = len(self.rset)
+
+ # Format 'expected', 'operator', 'assert'
+ testdata = (( expected, eq, 1),
+ ( expected+1, eq, 0),
+ ( expected-1, eq, 0),
+ ( expected, le, 1),
+ ( expected+1, le, 1),
+ ( expected-1, le, 0),
+ ( expected-1, gt, 1),
+ ( expected, gt, 0),
+ ( expected+1, gt, 0),
+ ( expected+1, lt, 1),
+ ( expected, lt, 0),
+ ( expected-1, lt, 0))
+
+ for (expected, operator, assertion) in testdata:
+ selector = multi_lines_rset(expected, operator)
+ yield self.assertEqual, selector(None, self.req, rset=self.rset), assertion
+
+ def test_match_kwargs_default(self):
+ selector = match_kwargs( set( ('a', 'b') ) )
+ self.assertEqual(selector(None, None, a=1, b=2), 2)
+ self.assertEqual(selector(None, None, a=1), 0)
+ self.assertEqual(selector(None, None, c=1), 0)
+ self.assertEqual(selector(None, None, a=1, c=1), 0)
+
+ def test_match_kwargs_any(self):
+ selector = match_kwargs( set( ('a', 'b') ), mode='any')
+ self.assertEqual(selector(None, None, a=1, b=2), 2)
+ self.assertEqual(selector(None, None, a=1), 1)
+ self.assertEqual(selector(None, None, c=1), 0)
+ self.assertEqual(selector(None, None, a=1, c=1), 1)
+
+
+class ScoreEntitySelectorTC(CubicWebTC):
+
+ def test_intscore_entity_selector(self):
+ req = self.request()
+ rset = req.execute('Any E WHERE E eid 1')
+ selector = score_entity(lambda x: None)
+ self.assertEqual(selector(None, req, rset=rset), 0)
+ selector = score_entity(lambda x: "something")
+ self.assertEqual(selector(None, req, rset=rset), 1)
+ selector = score_entity(lambda x: object)
+ self.assertEqual(selector(None, req, rset=rset), 1)
+ rset = req.execute('Any G LIMIT 2 WHERE G is CWGroup')
+ selector = score_entity(lambda x: 10)
+ self.assertEqual(selector(None, req, rset=rset), 20)
+ selector = score_entity(lambda x: 10, mode='any')
+ self.assertEqual(selector(None, req, rset=rset), 10)
+
+ def test_rql_condition_entity(self):
+ req = self.request()
+ selector = rql_condition('X identity U')
+ rset = req.user.as_rset()
+ self.assertEqual(selector(None, req, rset=rset), 1)
+ self.assertEqual(selector(None, req, entity=req.user), 1)
+ self.assertEqual(selector(None, req), 0)
+
+ def test_rql_condition_user(self):
+ req = self.request()
+ selector = rql_condition('U login "admin"', user_condition=True)
+ self.assertEqual(selector(None, req), 1)
+ selector = rql_condition('U login "toto"', user_condition=True)
+ self.assertEqual(selector(None, req), 0)
+
+if __name__ == '__main__':
+ unittest_main()
+
--- a/test/unittest_req.py Tue Jul 10 10:33:19 2012 +0200
+++ b/test/unittest_req.py Tue Jul 10 15:07:52 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.
@@ -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_selectors.py Tue Jul 10 10:33:19 2012 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,455 +0,0 @@
-# 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/>.
-"""unit tests for selectors mechanism"""
-from __future__ import with_statement
-
-from operator import eq, lt, le, gt
-from logilab.common.testlib import TestCase, unittest_main
-
-from cubicweb import Binary
-from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.appobject import Selector, AndSelector, OrSelector
-from cubicweb.selectors import (is_instance, adaptable, match_kwargs, match_user_groups,
- multi_lines_rset, score_entity, is_in_state,
- on_transition, rql_condition, relation_possible)
-from cubicweb.web import action
-
-
-class _1_(Selector):
- def __call__(self, *args, **kwargs):
- return 1
-
-class _0_(Selector):
- def __call__(self, *args, **kwargs):
- return 0
-
-def _2_(*args, **kwargs):
- return 2
-
-
-class SelectorsTC(TestCase):
- def test_basic_and(self):
- selector = _1_() & _1_()
- self.assertEqual(selector(None), 2)
- selector = _1_() & _0_()
- self.assertEqual(selector(None), 0)
- selector = _0_() & _1_()
- self.assertEqual(selector(None), 0)
-
- def test_basic_or(self):
- selector = _1_() | _1_()
- self.assertEqual(selector(None), 1)
- selector = _1_() | _0_()
- self.assertEqual(selector(None), 1)
- selector = _0_() | _1_()
- self.assertEqual(selector(None), 1)
- selector = _0_() | _0_()
- self.assertEqual(selector(None), 0)
-
- def test_selector_and_function(self):
- selector = _1_() & _2_
- self.assertEqual(selector(None), 3)
- selector = _2_ & _1_()
- self.assertEqual(selector(None), 3)
-
- def test_three_and(self):
- selector = _1_() & _1_() & _1_()
- self.assertEqual(selector(None), 3)
- selector = _1_() & _0_() & _1_()
- self.assertEqual(selector(None), 0)
- selector = _0_() & _1_() & _1_()
- self.assertEqual(selector(None), 0)
-
- def test_three_or(self):
- selector = _1_() | _1_() | _1_()
- self.assertEqual(selector(None), 1)
- selector = _1_() | _0_() | _1_()
- self.assertEqual(selector(None), 1)
- selector = _0_() | _1_() | _1_()
- self.assertEqual(selector(None), 1)
- selector = _0_() | _0_() | _0_()
- self.assertEqual(selector(None), 0)
-
- def test_composition(self):
- selector = (_1_() & _1_()) & (_1_() & _1_())
- self.assertTrue(isinstance(selector, AndSelector))
- self.assertEqual(len(selector.selectors), 4)
- self.assertEqual(selector(None), 4)
- selector = (_1_() & _0_()) | (_1_() & _1_())
- self.assertTrue(isinstance(selector, OrSelector))
- self.assertEqual(len(selector.selectors), 2)
- self.assertEqual(selector(None), 2)
-
- def test_search_selectors(self):
- sel = is_instance('something')
- self.assertIs(sel.search_selector(is_instance), sel)
- csel = AndSelector(sel, Selector())
- self.assertIs(csel.search_selector(is_instance), sel)
- csel = AndSelector(Selector(), sel)
- self.assertIs(csel.search_selector(is_instance), sel)
- self.assertIs(csel.search_selector((AndSelector, OrSelector)), csel)
- self.assertIs(csel.search_selector((OrSelector, AndSelector)), csel)
- self.assertIs(csel.search_selector((is_instance, score_entity)), sel)
- self.assertIs(csel.search_selector((score_entity, is_instance)), sel)
-
- def test_inplace_and(self):
- selector = _1_()
- selector &= _1_()
- selector &= _1_()
- self.assertEqual(selector(None), 3)
- selector = _1_()
- selector &= _0_()
- selector &= _1_()
- self.assertEqual(selector(None), 0)
- selector = _0_()
- selector &= _1_()
- selector &= _1_()
- self.assertEqual(selector(None), 0)
- selector = _0_()
- selector &= _0_()
- selector &= _0_()
- self.assertEqual(selector(None), 0)
-
- def test_inplace_or(self):
- selector = _1_()
- selector |= _1_()
- selector |= _1_()
- self.assertEqual(selector(None), 1)
- selector = _1_()
- selector |= _0_()
- selector |= _1_()
- self.assertEqual(selector(None), 1)
- selector = _0_()
- selector |= _1_()
- selector |= _1_()
- self.assertEqual(selector(None), 1)
- selector = _0_()
- selector |= _0_()
- selector |= _0_()
- self.assertEqual(selector(None), 0)
-
-
-class ImplementsSelectorTC(CubicWebTC):
- def test_etype_priority(self):
- req = self.request()
- f = req.create_entity('File', data_name=u'hop.txt', data=Binary('hop'))
- rset = f.as_rset()
- anyscore = is_instance('Any')(f.__class__, req, rset=rset)
- idownscore = adaptable('IDownloadable')(f.__class__, req, rset=rset)
- self.assertTrue(idownscore > anyscore, (idownscore, anyscore))
- filescore = is_instance('File')(f.__class__, req, rset=rset)
- self.assertTrue(filescore > idownscore, (filescore, idownscore))
-
- def test_etype_inheritance_no_yams_inheritance(self):
- cls = self.vreg['etypes'].etype_class('Personne')
- self.assertFalse(is_instance('Societe').score_class(cls, self.request()))
-
- def test_yams_inheritance(self):
- cls = self.vreg['etypes'].etype_class('Transition')
- self.assertEqual(is_instance('BaseTransition').score_class(cls, self.request()),
- 3)
-
- def test_outer_join(self):
- req = self.request()
- rset = req.execute('Any U,B WHERE B? bookmarked_by U, U login "anon"')
- self.assertEqual(is_instance('Bookmark')(None, req, rset=rset, row=0, col=1),
- 0)
-
-
-class WorkflowSelectorTC(CubicWebTC):
- def _commit(self):
- self.commit()
- self.wf_entity.cw_clear_all_caches()
-
- def setup_database(self):
- wf = self.shell().add_workflow("wf_test", 'StateFull', default=True)
- created = wf.add_state('created', initial=True)
- validated = wf.add_state('validated')
- abandoned = wf.add_state('abandoned')
- wf.add_transition('validate', created, validated, ('managers',))
- wf.add_transition('forsake', (created, validated,), abandoned, ('managers',))
-
- def setUp(self):
- super(WorkflowSelectorTC, self).setUp()
- self.req = self.request()
- self.wf_entity = self.req.create_entity('StateFull', name=u'')
- self.rset = self.wf_entity.as_rset()
- self.adapter = self.wf_entity.cw_adapt_to('IWorkflowable')
- self._commit()
- self.assertEqual(self.adapter.state, 'created')
- # enable debug mode to state/transition validation on the fly
- self.vreg.config.debugmode = True
-
- def tearDown(self):
- self.vreg.config.debugmode = False
- super(WorkflowSelectorTC, self).tearDown()
-
- def test_is_in_state(self):
- for state in ('created', 'validated', 'abandoned'):
- selector = is_in_state(state)
- self.assertEqual(selector(None, self.req, rset=self.rset),
- state=="created")
-
- self.adapter.fire_transition('validate')
- self._commit()
- self.assertEqual(self.adapter.state, 'validated')
-
- selector = is_in_state('created')
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
- selector = is_in_state('validated')
- self.assertEqual(selector(None, self.req, rset=self.rset), 1)
- selector = is_in_state('validated', 'abandoned')
- self.assertEqual(selector(None, self.req, rset=self.rset), 1)
- selector = is_in_state('abandoned')
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-
- self.adapter.fire_transition('forsake')
- self._commit()
- self.assertEqual(self.adapter.state, 'abandoned')
-
- selector = is_in_state('created')
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
- selector = is_in_state('validated')
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
- selector = is_in_state('validated', 'abandoned')
- self.assertEqual(selector(None, self.req, rset=self.rset), 1)
- self.assertEqual(self.adapter.state, 'abandoned')
- self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-
- def test_is_in_state_unvalid_names(self):
- selector = is_in_state("unknown")
- with self.assertRaises(ValueError) as cm:
- selector(None, self.req, rset=self.rset)
- self.assertEqual(str(cm.exception),
- "wf_test: unknown state(s): unknown")
- selector = is_in_state("weird", "unknown", "created", "weird")
- with self.assertRaises(ValueError) as cm:
- selector(None, self.req, rset=self.rset)
- self.assertEqual(str(cm.exception),
- "wf_test: unknown state(s): unknown,weird")
-
- def test_on_transition(self):
- for transition in ('validate', 'forsake'):
- selector = on_transition(transition)
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-
- self.adapter.fire_transition('validate')
- self._commit()
- self.assertEqual(self.adapter.state, 'validated')
-
- selector = on_transition("validate")
- self.assertEqual(selector(None, self.req, rset=self.rset), 1)
- selector = on_transition("validate", "forsake")
- self.assertEqual(selector(None, self.req, rset=self.rset), 1)
- selector = on_transition("forsake")
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-
- self.adapter.fire_transition('forsake')
- self._commit()
- self.assertEqual(self.adapter.state, 'abandoned')
-
- selector = on_transition("validate")
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
- selector = on_transition("validate", "forsake")
- self.assertEqual(selector(None, self.req, rset=self.rset), 1)
- selector = on_transition("forsake")
- self.assertEqual(selector(None, self.req, rset=self.rset), 1)
-
- def test_on_transition_unvalid_names(self):
- selector = on_transition("unknown")
- with self.assertRaises(ValueError) as cm:
- selector(None, self.req, rset=self.rset)
- self.assertEqual(str(cm.exception),
- "wf_test: unknown transition(s): unknown")
- selector = on_transition("weird", "unknown", "validate", "weird")
- with self.assertRaises(ValueError) as cm:
- selector(None, self.req, rset=self.rset)
- self.assertEqual(str(cm.exception),
- "wf_test: unknown transition(s): unknown,weird")
-
- def test_on_transition_with_no_effect(self):
- """selector will not be triggered with `change_state()`"""
- self.adapter.change_state('validated')
- self._commit()
- self.assertEqual(self.adapter.state, 'validated')
-
- selector = on_transition("validate")
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
- selector = on_transition("validate", "forsake")
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
- selector = on_transition("forsake")
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
-
-
-class RelationPossibleTC(CubicWebTC):
-
- def test_rqlst_1(self):
- req = self.request()
- selector = relation_possible('in_group')
- select = self.vreg.parse(req, 'Any X WHERE X is CWUser').children[0]
- score = selector(None, req, rset=1,
- select=select, filtered_variable=select.defined_vars['X'])
- self.assertEqual(score, 1)
-
- def test_rqlst_2(self):
- req = self.request()
- selector = relation_possible('in_group')
- select = self.vreg.parse(req, 'Any 1, COUNT(X) WHERE X is CWUser, X creation_date XD, '
- 'Y creation_date YD, Y is CWGroup '
- 'HAVING DAY(XD)=DAY(YD)').children[0]
- score = selector(None, req, rset=1,
- select=select, filtered_variable=select.defined_vars['X'])
- self.assertEqual(score, 1)
-
-
-class MatchUserGroupsTC(CubicWebTC):
- def test_owners_group(self):
- """tests usage of 'owners' group with match_user_group"""
- class SomeAction(action.Action):
- __regid__ = 'yo'
- category = 'foo'
- __select__ = match_user_groups('owners')
- self.vreg._loadedmods[__name__] = {}
- self.vreg.register(SomeAction)
- SomeAction.__registered__(self.vreg['actions'])
- self.assertTrue(SomeAction in self.vreg['actions']['yo'], self.vreg['actions'])
- try:
- # login as a simple user
- req = self.request()
- self.create_user(req, 'john')
- self.login('john')
- # it should not be possible to use SomeAction not owned objects
- req = self.request()
- rset = req.execute('Any G WHERE G is CWGroup, G name "managers"')
- self.assertFalse('yo' in dict(self.pactions(req, rset)))
- # insert a new card, and check that we can use SomeAction on our object
- self.execute('INSERT Card C: C title "zoubidou"')
- self.commit()
- req = self.request()
- rset = req.execute('Card C WHERE C title "zoubidou"')
- self.assertTrue('yo' in dict(self.pactions(req, rset)), self.pactions(req, rset))
- # make sure even managers can't use the action
- self.restore_connection()
- req = self.request()
- rset = req.execute('Card C WHERE C title "zoubidou"')
- self.assertFalse('yo' in dict(self.pactions(req, rset)))
- finally:
- del self.vreg[SomeAction.__registry__][SomeAction.__regid__]
-
-
-class MultiLinesRsetSelectorTC(CubicWebTC):
- def setUp(self):
- super(MultiLinesRsetSelectorTC, self).setUp()
- self.req = self.request()
- self.req.execute('INSERT CWGroup G: G name "group1"')
- self.req.execute('INSERT CWGroup G: G name "group2"')
- self.commit()
- self.rset = self.req.execute('Any G WHERE G is CWGroup')
-
- def test_default_op_in_selector(self):
- expected = len(self.rset)
- selector = multi_lines_rset(expected)
- self.assertEqual(selector(None, self.req, rset=self.rset), 1)
- self.assertEqual(selector(None, self.req, None), 0)
- selector = multi_lines_rset(expected + 1)
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
- self.assertEqual(selector(None, self.req, None), 0)
- selector = multi_lines_rset(expected - 1)
- self.assertEqual(selector(None, self.req, rset=self.rset), 0)
- self.assertEqual(selector(None, self.req, None), 0)
-
- def test_without_rset(self):
- expected = len(self.rset)
- selector = multi_lines_rset(expected)
- self.assertEqual(selector(None, self.req, None), 0)
- selector = multi_lines_rset(expected + 1)
- self.assertEqual(selector(None, self.req, None), 0)
- selector = multi_lines_rset(expected - 1)
- self.assertEqual(selector(None, self.req, None), 0)
-
- def test_with_operators(self):
- expected = len(self.rset)
-
- # Format 'expected', 'operator', 'assert'
- testdata = (( expected, eq, 1),
- ( expected+1, eq, 0),
- ( expected-1, eq, 0),
- ( expected, le, 1),
- ( expected+1, le, 1),
- ( expected-1, le, 0),
- ( expected-1, gt, 1),
- ( expected, gt, 0),
- ( expected+1, gt, 0),
- ( expected+1, lt, 1),
- ( expected, lt, 0),
- ( expected-1, lt, 0))
-
- for (expected, operator, assertion) in testdata:
- selector = multi_lines_rset(expected, operator)
- yield self.assertEqual, selector(None, self.req, rset=self.rset), assertion
-
- def test_match_kwargs_default(self):
- selector = match_kwargs( set( ('a', 'b') ) )
- self.assertEqual(selector(None, None, a=1, b=2), 2)
- self.assertEqual(selector(None, None, a=1), 0)
- self.assertEqual(selector(None, None, c=1), 0)
- self.assertEqual(selector(None, None, a=1, c=1), 0)
-
- def test_match_kwargs_any(self):
- selector = match_kwargs( set( ('a', 'b') ), mode='any')
- self.assertEqual(selector(None, None, a=1, b=2), 2)
- self.assertEqual(selector(None, None, a=1), 1)
- self.assertEqual(selector(None, None, c=1), 0)
- self.assertEqual(selector(None, None, a=1, c=1), 1)
-
-
-class ScoreEntitySelectorTC(CubicWebTC):
-
- def test_intscore_entity_selector(self):
- req = self.request()
- rset = req.execute('Any E WHERE E eid 1')
- selector = score_entity(lambda x: None)
- self.assertEqual(selector(None, req, rset=rset), 0)
- selector = score_entity(lambda x: "something")
- self.assertEqual(selector(None, req, rset=rset), 1)
- selector = score_entity(lambda x: object)
- self.assertEqual(selector(None, req, rset=rset), 1)
- rset = req.execute('Any G LIMIT 2 WHERE G is CWGroup')
- selector = score_entity(lambda x: 10)
- self.assertEqual(selector(None, req, rset=rset), 20)
- selector = score_entity(lambda x: 10, mode='any')
- self.assertEqual(selector(None, req, rset=rset), 10)
-
- def test_rql_condition_entity(self):
- req = self.request()
- selector = rql_condition('X identity U')
- rset = req.user.as_rset()
- self.assertEqual(selector(None, req, rset=rset), 1)
- self.assertEqual(selector(None, req, entity=req.user), 1)
- self.assertEqual(selector(None, req), 0)
-
- def test_rql_condition_user(self):
- req = self.request()
- selector = rql_condition('U login "admin"', user_condition=True)
- self.assertEqual(selector(None, req), 1)
- selector = rql_condition('U login "toto"', user_condition=True)
- self.assertEqual(selector(None, req), 0)
-
-if __name__ == '__main__':
- unittest_main()
-
--- a/test/unittest_vregistry.py Tue Jul 10 10:33:19 2012 +0200
+++ b/test/unittest_vregistry.py Tue Jul 10 15:07:52 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.
@@ -22,7 +22,7 @@
from cubicweb import CW_SOFTWARE_ROOT as BASE
from cubicweb.appobject import AppObject
-from cubicweb.cwvreg import CubicWebVRegistry, UnknownProperty
+from cubicweb.cwvreg import CWRegistryStore, UnknownProperty
from cubicweb.devtools import TestServerConfiguration
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.view import EntityAdapter
@@ -39,7 +39,7 @@
def setUp(self):
config = TestServerConfiguration('data')
- self.vreg = CubicWebVRegistry(config)
+ self.vreg = CWRegistryStore(config)
config.bootstrap_cubes()
self.vreg.schema = config.load_schema()
--- a/transaction.py Tue Jul 10 10:33:19 2012 +0200
+++ b/transaction.py Tue Jul 10 15:07:52 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/view.py Tue Jul 10 10:33:19 2012 +0200
+++ b/view.py Tue Jul 10 15:07:52 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,17 +26,17 @@
from functools import partial
from logilab.common.deprecation import deprecated
+from logilab.common.registry import classid, yes
from logilab.mtconverter import xml_escape
from rql import nodes
from cubicweb import NotAnEntity
-from cubicweb.selectors import yes, non_final_entity, nonempty_rset, none_rset
+from cubicweb.predicates import non_final_entity, nonempty_rset, none_rset
from cubicweb.appobject import AppObject
from cubicweb.utils import UStringIO, HTMLStream
from cubicweb.uilib import domid, js
from cubicweb.schema import display_name
-from cubicweb.vregistry import classid
# robots control
NOINDEX = u'<meta name="ROBOTS" content="NOINDEX" />'
--- a/vregistry.py Tue Jul 10 10:33:19 2012 +0200
+++ b/vregistry.py Tue Jul 10 15:07:52 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,487 +15,9 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""
-* the vregistry handles various types of objects interacting
- together. The vregistry handles registration of dynamically loaded
- objects and provides a convenient api to access those objects
- according to a context
-
-* to interact with the vregistry, objects should inherit from the
- AppObject abstract class
-
-* the selection procedure has been generalized by delegating to a
- selector, which is responsible to score the appobject according to the
- current state (req, rset, row, col). At the end of the selection, if
- a appobject class has been found, an instance of this class is
- returned. The selector is instantiated at appobject registration
-"""
-
-__docformat__ = "restructuredtext en"
-
-import sys
-from os import listdir, stat
-from os.path import dirname, join, realpath, isdir, exists
-from logging import getLogger
from warnings import warn
-
-from logilab.common.deprecation import deprecated, class_moved
-from logilab.common.logging_ext import set_log_methods
-
-from cubicweb import CW_SOFTWARE_ROOT
-from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject
-from cubicweb.appobject import AppObject, class_regid
-
-
-def _toload_info(path, extrapath, _toload=None):
- """return a dictionary of <modname>: <modpath> and an ordered list of
- (file, module name) to load
- """
- from logilab.common.modutils import modpath_from_file
- if _toload is None:
- assert isinstance(path, list)
- _toload = {}, []
- for fileordir in path:
- if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
- subfiles = [join(fileordir, fname) for fname in listdir(fileordir)]
- _toload_info(subfiles, extrapath, _toload)
- elif fileordir[-3:] == '.py':
- modpath = modpath_from_file(fileordir, extrapath)
- # omit '__init__' from package's name to avoid loading that module
- # once for each name when it is imported by some other appobject
- # module. This supposes import in modules are done as::
- #
- # from package import something
- #
- # not::
- #
- # from package.__init__ import something
- #
- # which seems quite correct.
- if modpath[-1] == '__init__':
- modpath.pop()
- modname = '.'.join(modpath)
- _toload[0][modname] = fileordir
- _toload[1].append((fileordir, modname))
- return _toload
-
-
-def classid(cls):
- """returns a unique identifier for an appobject class"""
- return '%s.%s' % (cls.__module__, cls.__name__)
-
-def class_registries(cls, registryname):
- if registryname:
- return (registryname,)
- return cls.__registries__
-
-
-class Registry(dict):
-
- def __init__(self, config):
- super(Registry, self).__init__()
- self.config = config
-
- def __getitem__(self, name):
- """return the registry (dictionary of class objects) associated to
- this name
- """
- try:
- return super(Registry, self).__getitem__(name)
- except KeyError:
- raise ObjectNotFound(name), None, sys.exc_info()[-1]
-
- def initialization_completed(self):
- for appobjects in self.itervalues():
- for appobjectcls in appobjects:
- appobjectcls.__registered__(self)
-
- def register(self, obj, oid=None, clear=False):
- """base method to add an object in the registry"""
- assert not '__abstract__' in obj.__dict__
- oid = oid or class_regid(obj)
- assert oid
- if clear:
- appobjects = self[oid] = []
- else:
- appobjects = self.setdefault(oid, [])
- assert not obj in appobjects, \
- 'object %s is already registered' % obj
- appobjects.append(obj)
-
- def register_and_replace(self, obj, replaced):
- # XXXFIXME this is a duplication of unregister()
- # remove register_and_replace in favor of unregister + register
- # or simplify by calling unregister then register here
- if not isinstance(replaced, basestring):
- replaced = classid(replaced)
- # prevent from misspelling
- assert obj is not replaced, 'replacing an object by itself: %s' % obj
- registered_objs = self.get(class_regid(obj), ())
- for index, registered in enumerate(registered_objs):
- if classid(registered) == replaced:
- del registered_objs[index]
- break
- else:
- self.warning('trying to replace an unregistered view %s by %s',
- replaced, obj)
- self.register(obj)
-
- def unregister(self, obj):
- clsid = classid(obj)
- oid = class_regid(obj)
- for registered in self.get(oid, ()):
- # use classid() to compare classes because vreg will probably
- # have its own version of the class, loaded through execfile
- if classid(registered) == clsid:
- self[oid].remove(registered)
- break
- else:
- self.warning('can\'t remove %s, no id %s in the registry',
- clsid, oid)
-
- def all_objects(self):
- """return a list containing all objects in this registry.
- """
- result = []
- for objs in self.values():
- result += objs
- return result
-
- # dynamic selection methods ################################################
-
- def object_by_id(self, oid, *args, **kwargs):
- """return object with the `oid` identifier. Only one object is expected
- to be found.
-
- raise :exc:`ObjectNotFound` if not object with id <oid> in <registry>
-
- raise :exc:`AssertionError` if there is more than one object there
- """
- objects = self[oid]
- assert len(objects) == 1, objects
- return objects[0](*args, **kwargs)
-
- def select(self, __oid, *args, **kwargs):
- """return the most specific object among those with the given oid
- according to the given context.
-
- raise :exc:`ObjectNotFound` if not object with id <oid> in <registry>
-
- raise :exc:`NoSelectableObject` if not object apply
- """
- obj = self._select_best(self[__oid], *args, **kwargs)
- if obj is None:
- raise NoSelectableObject(args, kwargs, self[__oid] )
- return obj
-
- def select_or_none(self, __oid, *args, **kwargs):
- """return the most specific object among those with the given oid
- according to the given context, or None if no object applies.
- """
- try:
- return self.select(__oid, *args, **kwargs)
- except (NoSelectableObject, ObjectNotFound):
- return None
-
- def possible_objects(self, *args, **kwargs):
- """return an iterator on possible objects in this registry for the given
- context
- """
- for appobjects in self.itervalues():
- obj = self._select_best(appobjects, *args, **kwargs)
- if obj is None:
- continue
- yield obj
-
- def _select_best(self, appobjects, *args, **kwargs):
- """return an instance of the most specific object according
- to parameters
-
- return None if not object apply (don't raise `NoSelectableObject` since
- it's costly when searching appobjects using `possible_objects`
- (e.g. searching for hooks).
- """
- score, winners = 0, None
- for appobject in appobjects:
- appobjectscore = appobject.__select__(appobject, *args, **kwargs)
- if appobjectscore > score:
- score, winners = appobjectscore, [appobject]
- elif appobjectscore > 0 and appobjectscore == score:
- winners.append(appobject)
- if winners is None:
- return None
- if len(winners) > 1:
- # log in production environement / test, error while debugging
- msg = 'select ambiguity: %s\n(args: %s, kwargs: %s)'
- if self.config.debugmode or self.config.mode == 'test':
- # raise bare exception in debug mode
- raise Exception(msg % (winners, args, kwargs.keys()))
- self.error(msg, winners, args, kwargs.keys())
- # return the result of calling the appobject
- return winners[0](*args, **kwargs)
-
- # these are overridden by set_log_methods below
- # only defining here to prevent pylint from complaining
- info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
-
-
-class VRegistry(dict):
- """class responsible to register, propose and select the various
- elements used to build the web interface. Currently, we have templates,
- views, actions and components.
- """
+from logilab.common.deprecation import class_moved
+warn('[3.15] moved to logilab.common.registry', DeprecationWarning, stacklevel=2)
+from logilab.common.registry import *
- def __init__(self, config):
- super(VRegistry, self).__init__()
- self.config = config
- # need to clean sys.path this to avoid import confusion pb (i.e. having
- # the same module loaded as 'cubicweb.web.views' subpackage and as
- # views' or 'web.views' subpackage. This is mainly for testing purpose,
- # we should'nt need this in production environment
- for webdir in (join(dirname(realpath(__file__)), 'web'),
- join(dirname(__file__), 'web')):
- if webdir in sys.path:
- sys.path.remove(webdir)
- if CW_SOFTWARE_ROOT in sys.path:
- sys.path.remove(CW_SOFTWARE_ROOT)
-
- def reset(self):
- # don't use self.clear, we want to keep existing subdictionaries
- for subdict in self.itervalues():
- subdict.clear()
- self._lastmodifs = {}
-
- def __getitem__(self, name):
- """return the registry (dictionary of class objects) associated to
- this name
- """
- try:
- return super(VRegistry, self).__getitem__(name)
- except KeyError:
- raise RegistryNotFound(name), None, sys.exc_info()[-1]
-
- # methods for explicit (un)registration ###################################
-
- # default class, when no specific class set
- REGISTRY_FACTORY = {None: Registry}
-
- def registry_class(self, regid):
- try:
- return self.REGISTRY_FACTORY[regid]
- except KeyError:
- return self.REGISTRY_FACTORY[None]
-
- def setdefault(self, regid):
- try:
- return self[regid]
- except KeyError:
- self[regid] = self.registry_class(regid)(self.config)
- return self[regid]
-
-# def clear(self, key):
-# regname, oid = key.split('.')
-# self[regname].pop(oid, None)
-
- def register_all(self, objects, modname, butclasses=()):
- """register all `objects` given. Objects which are not from the module
- `modname` or which are in `butclasses` won't be registered.
-
- Typical usage is:
-
- .. sourcecode:: python
-
- vreg.register_all(globals().values(), __name__, (ClassIWantToRegisterExplicitly,))
-
- So you get partially automatic registration, keeping manual registration
- for some object (to use
- :meth:`~cubicweb.cwvreg.CubicWebRegistry.register_and_replace` for
- instance)
- """
- for obj in objects:
- try:
- if obj.__module__ != modname or obj in butclasses:
- continue
- oid = class_regid(obj)
- except AttributeError:
- continue
- if oid and not '__abstract__' in obj.__dict__:
- self.register(obj, oid=oid)
-
- def register(self, obj, registryname=None, oid=None, clear=False):
- """register `obj` application object into `registryname` or
- `obj.__registry__` if not specified, with identifier `oid` or
- `obj.__regid__` if not specified.
-
- If `clear` is true, all objects with the same identifier will be
- previously unregistered.
- """
- assert not '__abstract__' in obj.__dict__
- try:
- vname = obj.__name__
- except AttributeError:
- # XXX may occurs?
- vname = obj.__class__.__name__
- for registryname in class_registries(obj, registryname):
- registry = self.setdefault(registryname)
- registry.register(obj, oid=oid, clear=clear)
- self.debug('register %s in %s[\'%s\']',
- vname, registryname, oid or class_regid(obj))
- self._loadedmods.setdefault(obj.__module__, {})[classid(obj)] = obj
-
- def unregister(self, obj, registryname=None):
- """unregister `obj` application object from the registry `registryname` or
- `obj.__registry__` if not specified.
- """
- for registryname in class_registries(obj, registryname):
- self[registryname].unregister(obj)
-
- def register_and_replace(self, obj, replaced, registryname=None):
- """register `obj` application object into `registryname` or
- `obj.__registry__` if not specified. If found, the `replaced` object
- will be unregistered first (else a warning will be issued as it's
- generally unexpected).
- """
- for registryname in class_registries(obj, registryname):
- self[registryname].register_and_replace(obj, replaced)
-
- # initialization methods ###################################################
-
- def init_registration(self, path, extrapath=None):
- self.reset()
- # compute list of all modules that have to be loaded
- self._toloadmods, filemods = _toload_info(path, extrapath)
- # XXX is _loadedmods still necessary ? It seems like it's useful
- # to avoid loading same module twice, especially with the
- # _load_ancestors_then_object logic but this needs to be checked
- self._loadedmods = {}
- return filemods
-
- def register_objects(self, path, extrapath=None):
- # load views from each directory in the instance's path
- filemods = self.init_registration(path, extrapath)
- for filepath, modname in filemods:
- self.load_file(filepath, modname)
- self.initialization_completed()
-
- def initialization_completed(self):
- for regname, reg in self.iteritems():
- reg.initialization_completed()
-
- def _mdate(self, filepath):
- try:
- return stat(filepath)[-2]
- except OSError:
- # this typically happens on emacs backup files (.#foo.py)
- self.warning('Unable to load %s. It is likely to be a backup file',
- filepath)
- return None
-
- def is_reload_needed(self, path):
- """return True if something module changed and the registry should be
- reloaded
- """
- lastmodifs = self._lastmodifs
- for fileordir in path:
- if isdir(fileordir) and exists(join(fileordir, '__init__.py')):
- if self.is_reload_needed([join(fileordir, fname)
- for fname in listdir(fileordir)]):
- return True
- elif fileordir[-3:] == '.py':
- mdate = self._mdate(fileordir)
- if mdate is None:
- continue # backup file, see _mdate implementation
- elif "flymake" in fileordir:
- # flymake + pylint in use, don't consider these they will corrupt the registry
- continue
- if fileordir not in lastmodifs or lastmodifs[fileordir] < mdate:
- self.info('File %s changed since last visit', fileordir)
- return True
- return False
-
- def load_file(self, filepath, modname):
- """load app objects from a python file"""
- from logilab.common.modutils import load_module_from_name
- if modname in self._loadedmods:
- return
- self._loadedmods[modname] = {}
- mdate = self._mdate(filepath)
- if mdate is None:
- return # backup file, see _mdate implementation
- elif "flymake" in filepath:
- # flymake + pylint in use, don't consider these they will corrupt the registry
- return
- # set update time before module loading, else we get some reloading
- # weirdness in case of syntax error or other error while importing the
- # module
- self._lastmodifs[filepath] = mdate
- # load the module
- module = load_module_from_name(modname)
- self.load_module(module)
-
- def load_module(self, module):
- self.info('loading %s from %s', module.__name__, module.__file__)
- if hasattr(module, 'registration_callback'):
- module.registration_callback(self)
- else:
- for objname, obj in vars(module).items():
- if objname.startswith('_'):
- continue
- self._load_ancestors_then_object(module.__name__, obj)
-
- def _load_ancestors_then_object(self, modname, appobjectcls):
- """handle automatic appobject class registration:
-
- - first ensure parent classes are already registered
-
- - class with __abstract__ == True in their local dictionnary or
- with a name starting with an underscore are not registered
-
- - appobject class needs to have __registry__ and __regid__ attributes
- set to a non empty string to be registered.
- """
- # imported classes
- objmodname = getattr(appobjectcls, '__module__', None)
- if objmodname != modname:
- if objmodname in self._toloadmods:
- self.load_file(self._toloadmods[objmodname], objmodname)
- return
- # skip non registerable object
- try:
- if not issubclass(appobjectcls, AppObject):
- return
- except TypeError:
- return
- clsid = classid(appobjectcls)
- if clsid in self._loadedmods[modname]:
- return
- self._loadedmods[modname][clsid] = appobjectcls
- for parent in appobjectcls.__bases__:
- self._load_ancestors_then_object(modname, parent)
- if (appobjectcls.__dict__.get('__abstract__')
- or appobjectcls.__name__[0] == '_'
- or not appobjectcls.__registries__
- or not class_regid(appobjectcls)):
- return
- try:
- self.register(appobjectcls)
- except Exception, ex:
- if self.config.mode in ('test', 'dev'):
- raise
- self.exception('appobject %s registration failed: %s',
- appobjectcls, ex)
- # these are overridden by set_log_methods below
- # only defining here to prevent pylint from complaining
- info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
-
-
-# init logging
-set_log_methods(VRegistry, getLogger('cubicweb.vreg'))
-set_log_methods(Registry, getLogger('cubicweb.registry'))
-
-
-# XXX bw compat functions #####################################################
-
-from cubicweb.appobject import objectify_selector, AndSelector, OrSelector, Selector
-
-Selector = class_moved(Selector)
+VRegistry = class_moved(RegistryStore, old_name='VRegistry', message='[3.15] VRegistry moved to logilab.common.registry as RegistryStore')
--- a/web/__init__.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/__init__.py Tue Jul 10 15:07:52 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/_exceptions.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/_exceptions.py Tue Jul 10 15:07:52 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/action.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/action.py Tue Jul 10 15:07:52 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.
@@ -68,8 +68,8 @@
_ = unicode
from cubicweb import target
-from cubicweb.selectors import (partial_relation_possible, match_search_state,
- one_line_rset)
+from cubicweb.predicates import (partial_relation_possible, match_search_state,
+ one_line_rset)
from cubicweb.appobject import AppObject
--- a/web/application.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/application.py Tue Jul 10 15:07:52 2012 +0200
@@ -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
@@ -276,7 +281,7 @@
vreg=None):
self.info('starting web instance from %s', config.apphome)
if vreg is None:
- vreg = cwvreg.CubicWebVRegistry(config)
+ vreg = cwvreg.CWRegistryStore(config)
self.vreg = vreg
# connect to the repository and get instance's schema
self.repo = config.repository(vreg)
@@ -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,78 @@
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()
+ # 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,87 +423,87 @@
: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:
+ # handle redirect
+ # - comply to ex status
+ # - set header field
+ #
+ # Redirect Maybe be is raised by edit controller when
+ # everything went fine, so try to commit
+ 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
+ result = ''
+ 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:
+ req.status_out = httplib.CONFLICT
+ 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 defence 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 handler
def validation_error_handler(self, req, ex):
ex.errors = dict((k, v) for k, v in ex.errors.items())
if '__errorurl' in req.form:
@@ -440,18 +516,21 @@
# 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 ''
+ 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))
req.set_header('Cache-Control', 'no-cache')
req.remove_header('Etag')
req.reset_message()
req.reset_headers()
- if req.json_request:
- raise RemoteCallFailed(unicode(ex))
+ if req.ajax_request:
+ return ajax_error_handler(req, ex)
try:
req.data['ex'] = ex
if tb:
@@ -462,7 +541,29 @@
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 +577,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/box.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/box.py Tue Jul 10 15:07:52 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.
@@ -25,7 +25,7 @@
from cubicweb import Unauthorized, role as get_role
from cubicweb.schema import display_name
-from cubicweb.selectors import no_cnx, one_line_rset
+from cubicweb.predicates import no_cnx, one_line_rset
from cubicweb.view import View
from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
--- a/web/component.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/component.py Tue Jul 10 15:07:52 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,7 @@
from cubicweb.uilib import js, domid
from cubicweb.utils import json_dumps, js_href
from cubicweb.view import ReloadableMixIn, Component
-from cubicweb.selectors import (no_cnx, paginated_rset, one_line_rset,
+from cubicweb.predicates import (no_cnx, paginated_rset, one_line_rset,
non_final_entity, partial_relation_possible,
partial_has_related_entities)
from cubicweb.appobject import AppObject
--- a/web/controller.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/controller.py Tue Jul 10 15:07:52 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,8 +20,8 @@
__docformat__ = "restructuredtext en"
from logilab.mtconverter import xml_escape
+from logilab.common.registry import yes
-from cubicweb.selectors import yes
from cubicweb.appobject import AppObject
from cubicweb.mail import format_mail
from cubicweb.web import LOGGER, Redirect, RequestError
--- a/web/data/cubicweb.ajax.box.js Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/cubicweb.ajax.box.js Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/cubicweb.ajax.js Tue Jul 10 15:07:52 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);
+ }
}
}
});
@@ -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');
}
/* 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/cubicweb.css Tue Jul 10 15:07:52 2012 +0200
@@ -82,7 +82,7 @@
letter-spacing: 0.015em;
padding: 0.6em;
margin: 0 2em 1.7em;
- background-color: %(listingHihligthedBgColor)s;
+ background-color: %(listingHighlightedBgColor)s;
border: 1px solid %(listingBorderColor)s;
}
@@ -242,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;
@@ -575,7 +583,7 @@
vertical-align: bottom;
}
-input#norql{
+input.norql{
width:155px;
margin-right: 2px;
}
@@ -811,7 +819,7 @@
table.listing input,
table.listing textarea {
- background: %(listingHihligthedBgColor)s;
+ background: %(listingHighlightedBgColor)s;
}
table.htableForm label, table.oneRowTableForm label {
--- a/web/data/cubicweb.edition.js Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/cubicweb.edition.js Tue Jul 10 15:07:52 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);
@@ -591,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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/cubicweb.facets.js Tue Jul 10 15:07:52 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');
@@ -87,7 +87,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 +99,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 +118,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.iprogress.css Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/cubicweb.iprogress.css Tue Jul 10 15:07:52 2012 +0200
@@ -62,11 +62,11 @@
}
table.progress tr.highlighted {
- background-color: %(listingHihligthedBgColor)s;
+ background-color: %(listingHighlightedBgColor)s;
}
table.progress tr.highlighted .progressbarback {
- border: 1px solid %(listingHihligthedBgColor)s;
+ border: 1px solid %(listingHighlightedBgColor)s;
}
table.progress .progressbarback {
--- a/web/data/cubicweb.js Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/cubicweb.js Tue Jul 10 15:07:52 2012 +0200
@@ -84,11 +84,13 @@
},
sortValueExtraction: function (node) {
- var sortvalue = jQuery(node).attr('cubicweb:sortvalue');
- if (sortvalue === undefined) {
- return '';
- }
- return cw.evalJSON(sortvalue);
+ var $node = $(node);
+ var sortvalue = $node.attr('cubicweb:sortvalue');
+ // No metadata found, use cell content as sort key
+ if (sortvalue === undefined) {
+ return $node.text();
+ }
+ return cw.evalJSON(sortvalue);
}
});
--- a/web/data/cubicweb.old.css Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/cubicweb.old.css Tue Jul 10 15:07:52 2012 +0200
@@ -275,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 {
--- a/web/data/cubicweb.reledit.js Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/cubicweb.reledit.js Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/cubicweb.widgets.js Tue Jul 10 15:07:52 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);
}
--- a/web/data/uiprops.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/data/uiprops.py Tue Jul 10 15:07:52 2012 +0200
@@ -148,7 +148,7 @@
# table listing & co ###########################################################
listingBorderColor = '#ccc'
listingHeaderBgColor = '#efefef'
-listingHihligthedBgColor = '#fbfbfb'
+listingHighlightedBgColor = '#fbfbfb'
# puce
bulletDownImg = 'url("puce_down.png") 98% 6px no-repeat'
--- a/web/facet.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/facet.py Tue Jul 10 15:07:52 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.
@@ -59,6 +59,7 @@
from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
from logilab.common.compat import all
from logilab.common.deprecation import deprecated
+from logilab.common.registry import yes
from rql import nodes, utils
@@ -66,7 +67,7 @@
from cubicweb.schema import display_name
from cubicweb.uilib import css_em_num_value
from cubicweb.utils import make_uid
-from cubicweb.selectors import match_context_prop, partial_relation_possible, yes
+from cubicweb.predicates import match_context_prop, partial_relation_possible
from cubicweb.appobject import AppObject
from cubicweb.web import RequestError, htmlwidgets
--- a/web/formfields.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/formfields.py Tue Jul 10 15:07:52 2012 +0200
@@ -360,7 +360,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/http_headers.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/http_headers.py Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/httpcache.py Tue Jul 10 15:07:52 2012 +0200
@@ -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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/request.py Tue Jul 10 15:07:52 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.
@@ -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"""
- json_request = False # to be set to True by json controllers
+ """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 uiprops_)
+ self.uiprops = None
+ #: url for serving datadir (vary with https) (see 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
@@ -121,10 +141,41 @@
self.html_headers.define_var('pageid', pid, override=False)
self.pageid = pid
+ def _get_json_request(self):
+ warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
+ DeprecationWarning, stacklevel=2)
+ return self.ajax_request
+ def _set_json_request(self, value):
+ warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
+ DeprecationWarning, stacklevel=2)
+ 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 `Configuring the Web server`_)"""
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
@@ -259,7 +310,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
@@ -701,14 +751,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 #############
@@ -716,11 +785,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
@@ -732,12 +809,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:
@@ -785,12 +887,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
@@ -805,6 +901,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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/test/data/views.py Tue Jul 10 15:07:52 2012 +0200
@@ -21,12 +21,12 @@
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 +40,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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/test/unittest_application.py Tue Jul 10 15:07:52 2012 +0200
@@ -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()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_http.py Tue Jul 10 15:07:52 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_request.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/test/unittest_request.py Tue Jul 10 15:07:52 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_views_basecontrollers.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/test/unittest_views_basecontrollers.py Tue Jul 10 15:07:52 2012 +0200
@@ -19,16 +19,27 @@
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
from cubicweb import Binary, NoSelectableObject, ValidationError
from cubicweb.view import STRICT_DOCTYPE
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.utils import json_dumps
from cubicweb.uilib import rql_for_eid
-from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError
+from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError, RemoteCallFailed
from cubicweb.entities.authobjs import CWUser
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
def req_form(user):
@@ -85,7 +96,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')
@@ -104,7 +115,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)
@@ -125,7 +136,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')
@@ -151,7 +162,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)
@@ -173,7 +184,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)
@@ -193,7 +204,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')
@@ -256,7 +267,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)
@@ -268,7 +279,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'])
@@ -287,7 +298,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'])
@@ -307,7 +318,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')
@@ -329,7 +340,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')
@@ -341,7 +352,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
@@ -351,7 +362,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
@@ -361,7 +372,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)
@@ -377,7 +388,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)
@@ -395,7 +406,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)
@@ -421,7 +432,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)
@@ -441,7 +452,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)
@@ -479,7 +490,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')
@@ -509,12 +520,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:
@@ -557,11 +568,12 @@
-class JSONControllerTC(CubicWebTC):
+class AjaxControllerTC(CubicWebTC):
+ tested_controller = 'ajax'
def ctrl(self, req=None):
req = req or self.request(url='http://whatever.fr/')
- return self.vreg['controllers'].select('json', req)
+ return self.vreg['controllers'].select(self.tested_controller, req)
def setup_database(self):
req = self.request()
@@ -679,8 +691,168 @@
self.assertEqual(self.remote_call('format_date', '2007-01-01 12:00:00')[0],
json_dumps('2007/01/01'))
+ def test_ajaxfunc_noparameter(self):
+ @ajaxfunc
+ def foo(self, x, y):
+ 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, False)
+ self.assertEqual(appobject.output_type, None)
+ req = self.request()
+ f = appobject(req)
+ self.assertEqual(f(12, 13), 'hello')
+
+ def test_ajaxfunc_checkpageid(self):
+ @ajaxfunc(check_pageid=True)
+ def foo(self, x, y):
+ 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 = 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.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 = appobject(req)
+ self.assertEqual(f(12, 13), '25')
+class JSonControllerTC(AjaxControllerTC):
+ # NOTE: this class performs the same tests as AjaxController but with
+ # deprecated 'json' controller (i.e. check backward compatibility)
+ tested_controller = 'json'
+
+ def setUp(self):
+ super(JSonControllerTC, self).setUp()
+ self.exposed_remote_funcs = [fname for fname in dir(JSonController)
+ if fname.startswith('js_')]
+
+ def tearDown(self):
+ super(JSonControllerTC, self).tearDown()
+ for funcname in dir(JSonController):
+ # remove functions added dynamically during tests
+ if funcname.startswith('js_') and funcname not in self.exposed_remote_funcs:
+ delattr(JSonController, funcname)
+
+ def test_monkeypatch_jsoncontroller(self):
+ self.assertRaises(RemoteCallFailed, self.remote_call, 'foo')
+ @monkeypatch(JSonController)
+ def js_foo(self):
+ return u'hello'
+ res, req = self.remote_call('foo')
+ self.assertEqual(res, u'hello')
+
+ def test_monkeypatch_jsoncontroller_xhtmlize(self):
+ self.assertRaises(RemoteCallFailed, self.remote_call, 'foo')
+ @monkeypatch(JSonController)
+ @xhtmlize
+ def js_foo(self):
+ return u'hello'
+ res, req = self.remote_call('foo')
+ self.assertEqual(res,
+ '<?xml version="1.0"?>\n' + STRICT_DOCTYPE +
+ u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">hello</div>')
+
+ def test_monkeypatch_jsoncontroller_jsonize(self):
+ self.assertRaises(RemoteCallFailed, self.remote_call, 'foo')
+ @monkeypatch(JSonController)
+ @jsonize
+ def js_foo(self):
+ return 12
+ res, req = self.remote_call('foo')
+ 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('/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('.', cm.exception.location)
if __name__ == '__main__':
unittest_main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_views_staticcontrollers.py Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/test/unittest_viewselector.py Tue Jul 10 15:07:52 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.
@@ -22,16 +22,15 @@
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb import CW_SOFTWARE_ROOT as BASE, Binary, UnknownProperty
-from cubicweb.selectors import (match_user_groups, is_instance,
- specified_etype_implements, rql_condition,
- traced_selection)
+from cubicweb.predicates import (match_user_groups, is_instance,
+ specified_etype_implements, rql_condition)
from cubicweb.web import NoSelectableObject
from cubicweb.web.action import Action
from cubicweb.web.views import (
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
@@ -103,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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/actions.py Tue Jul 10 15:07:52 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.
@@ -23,10 +23,10 @@
from warnings import warn
from logilab.mtconverter import xml_escape
+from logilab.common.registry import objectify_predicate, yes
from cubicweb.schema import display_name
-from cubicweb.appobject import objectify_selector
-from cubicweb.selectors import (EntitySelector, yes,
+from cubicweb.predicates import (EntityPredicate,
one_line_rset, multi_lines_rset, one_etype_rset, relation_possible,
nonempty_rset, non_final_entity, score_entity,
authenticated_user, match_user_groups, match_search_state,
@@ -36,11 +36,11 @@
from cubicweb.web.views import linksearch_select_url, vid_from_rset
-class has_editable_relation(EntitySelector):
+class has_editable_relation(EntityPredicate):
"""accept if some relations for an entity found in the result set is
editable by the logged user.
- See `EntitySelector` documentation for behaviour when row is not specified.
+ See `EntityPredicate` documentation for behaviour when row is not specified.
"""
def score_entity(self, entity):
@@ -55,11 +55,11 @@
return 1
return 0
-@objectify_selector
+@objectify_predicate
def match_searched_etype(cls, req, rset=None, **kwargs):
return req.match_search_state(rset)
-@objectify_selector
+@objectify_predicate
def view_is_not_default_view(cls, req, rset=None, **kwargs):
# interesting if it propose another view than the current one
vid = req.form.get('vid')
@@ -67,7 +67,7 @@
return 1
return 0
-@objectify_selector
+@objectify_predicate
def addable_etype_empty_rset(cls, req, rset=None, **kwargs):
if rset is not None and not rset.rowcount:
rqlst = rset.syntax_tree()
@@ -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):
@@ -130,7 +142,7 @@
params = self._cw.form.copy()
for param in ('vid', '__message') + controller.NAV_FORM_PARAMETERS:
params.pop(param, None)
- if self._cw.json_request:
+ if self._cw.ajax_request:
path = 'view'
if self.cw_rset is not None:
params = {'rql': self.cw_rset.printable_rql()}
@@ -420,6 +432,7 @@
self._cw.add_js('cubicweb.rhythm.js')
return 'rhythm'
+
## default actions ui configuration ###########################################
addmenu = uicfg.actionbox_appearsin_addmenu
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/ajaxcontroller.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,462 @@
+# 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/>.
+#
+# (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-func`` cubicweb registry.
+
+.. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxController
+ :members:
+
+``ajax-funcs`` registry hosts exposed remote functions, that is
+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:
+
+.. sourcecode:: python
+
+ from cubicweb.predicates import mactch_user_groups
+ from cubicweb.web.views.ajaxcontroller import ajaxfunc
+
+ @ajaxfunc(output_type='json', selector=match_user_groups('managers'))
+ 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
+implement the ``__call__`` method:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views.ajaxcontroller import AjaxFunction
+ class ListUser(AjaxFunction):
+ __regid__ = 'list_users' # __regid__ is the name of the exposed function
+ __select__ = match_user_groups('managers')
+ output_type = 'json'
+
+ def __call__(self):
+ return [u for (u, ) in self._cw.execute('Any L WHERE U login L')]
+
+
+.. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxFunction
+ :members:
+
+.. autofunction:: cubicweb.web.views.ajaxcontroller.ajaxfunc
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+from functools import partial
+
+from logilab.common.date import strptime
+from logilab.common.registry import yes
+from logilab.common.deprecation import deprecated
+
+from cubicweb import ObjectNotFound, NoSelectableObject
+from cubicweb.appobject import AppObject
+from cubicweb.utils import json, json_dumps, UStringIO
+from cubicweb.uilib import exc_message
+from cubicweb.web import RemoteCallFailed, DirectResponse
+from cubicweb.web.controller import Controller
+from cubicweb.web.views import vid_from_rset
+from cubicweb.web.views import basecontrollers
+
+
+def optional_kwargs(extraargs):
+ if extraargs is None:
+ return {}
+ # we receive unicode keys which is not supported by the **syntax
+ return dict((str(key), value) for key, value in extraargs.iteritems())
+
+
+class AjaxController(Controller):
+ """AjaxController handles ajax remote calls from javascript
+
+ The following javascript function call:
+
+ .. sourcecode:: javascript
+
+ var d = asyncRemoteExec('foo', 12, "hello");
+ d.addCallback(function(result) {
+ alert('server response is: ' + result);
+ });
+
+ will generate an ajax HTTP GET on the following url::
+
+ BASE_URL/ajax?fname=foo&arg=12&arg="hello"
+
+ The AjaxController controller will therefore be selected to handle those URLs
+ and will itself select the :class:`cubicweb.web.views.ajaxcontroller.AjaxFunction`
+ matching the *fname* parameter.
+ """
+ __regid__ = 'ajax'
+
+ def publish(self, rset=None):
+ self._cw.ajax_request = True
+ try:
+ fname = self._cw.form['fname']
+ except KeyError:
+ raise RemoteCallFailed('no method specified')
+ try:
+ func = self._cw.vreg['ajax-func'].select(fname, self._cw)
+ except ObjectNotFound:
+ # function not found in the registry, inspect JSonController for
+ # backward compatibility
+ try:
+ func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
+ func = partial(func, self)
+ except AttributeError:
+ 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)
+ # no <arg> attribute means the callback takes no argument
+ args = self._cw.form.get('arg', ())
+ if not isinstance(args, (list, tuple)):
+ args = (args,)
+ try:
+ args = [json.loads(arg) for arg in args]
+ except ValueError, exc:
+ self.exception('error while decoding json arguments for '
+ 'js_%s: %s (err: %s)', fname, args, exc)
+ raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
+ try:
+ result = func(*args)
+ except (RemoteCallFailed, DirectResponse):
+ raise
+ except Exception, exc:
+ self.exception('an exception occurred while calling js_%s(%s): %s',
+ fname, args, exc)
+ raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
+ if result is None:
+ return ''
+ # get unicode on @htmlize methods, encoded string on @jsonize methods
+ elif isinstance(result, unicode):
+ return result.encode(self._cw.encoding)
+ return result
+
+class AjaxFunction(AppObject):
+ """
+ Attributes on this base class are:
+
+ :attr: `check_pageid`: make sure the pageid received is valid before proceeding
+ :attr: `output_type`:
+
+ - *None*: no processing, no change on content-type
+
+ - *json*: serialize with `json_dumps` and set *application/json*
+ content-type
+
+ - *xhtml*: wrap result in an XML node and forces HTML / XHTML
+ content-type (use ``_cw.html_content_type()``)
+
+ """
+ __registry__ = 'ajax-func'
+ __select__ = yes()
+ __abstract__ = True
+
+ check_pageid = False
+ output_type = None
+
+ @staticmethod
+ def _rebuild_posted_form(names, values, action=None):
+ form = {}
+ for name, value in zip(names, values):
+ # remove possible __action_xxx inputs
+ if name.startswith('__action'):
+ if action is None:
+ # strip '__action_' to get the actual action name
+ action = name[9:]
+ continue
+ # form.setdefault(name, []).append(value)
+ if name in form:
+ curvalue = form[name]
+ if isinstance(curvalue, list):
+ curvalue.append(value)
+ else:
+ form[name] = [curvalue, value]
+ else:
+ form[name] = value
+ # simulate click on __action_%s button to help the controller
+ if action:
+ form['__action_%s' % action] = u'whatever'
+ return form
+
+ def validate_form(self, action, names, values):
+ self._cw.form = self._rebuild_posted_form(names, values, action)
+ return basecontrollers._validate_form(self._cw, self._cw.vreg)
+
+ def _exec(self, rql, args=None, rocheck=True):
+ """json mode: execute RQL and return resultset as json"""
+ rql = rql.strip()
+ if rql.startswith('rql:'):
+ rql = rql[4:]
+ if rocheck:
+ self._cw.ensure_ro_rql(rql)
+ try:
+ return self._cw.execute(rql, args)
+ except Exception, ex:
+ self.exception("error in _exec(rql=%s): %s", rql, ex)
+ return None
+ return None
+
+ def _call_view(self, view, paginate=False, **kwargs):
+ divid = self._cw.form.get('divid')
+ # we need to call pagination before with the stream set
+ try:
+ stream = view.set_stream()
+ except AttributeError:
+ stream = UStringIO()
+ kwargs['w'] = stream.write
+ assert not paginate
+ if divid == 'pageContent':
+ # ensure divid isn't reused by the view (e.g. table view)
+ del self._cw.form['divid']
+ # mimick main template behaviour
+ stream.write(u'<div id="pageContent">')
+ vtitle = self._cw.form.get('vtitle')
+ if vtitle:
+ stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
+ paginate = True
+ nav_html = UStringIO()
+ if paginate and not view.handle_pagination:
+ view.paginate(w=nav_html.write)
+ stream.write(nav_html.getvalue())
+ if divid == 'pageContent':
+ stream.write(u'<div id="contentmain">')
+ view.render(**kwargs)
+ extresources = self._cw.html_headers.getvalue(skiphead=True)
+ if extresources:
+ stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
+ stream.write(extresources)
+ stream.write(u'</div>\n')
+ if divid == 'pageContent':
+ stream.write(u'</div>%s</div>' % nav_html.getvalue())
+ return stream.getvalue()
+
+
+def _ajaxfunc_factory(implementation, selector=yes(), _output_type=None,
+ _check_pageid=False, regid=None):
+ """converts a standard python function into an AjaxFunction appobject"""
+ class AnAjaxFunc(AjaxFunction):
+ __regid__ = regid or implementation.__name__
+ __select__ = selector
+ output_type = _output_type
+ check_pageid = _check_pageid
+
+ def serialize(self, content):
+ if self.output_type is None:
+ return content
+ elif self.output_type == 'xhtml':
+ self._cw.set_content_type(self._cw.html_content_type())
+ return ''.join((self._cw.document_surrounding_div(),
+ content.strip(), u'</div>'))
+ elif self.output_type == 'json':
+ self._cw.set_content_type('application/json')
+ return json_dumps(content)
+ raise RemoteCallFailed('no serializer found for output type %s'
+ % self.output_type)
+
+ def __call__(self, *args, **kwargs):
+ if self.check_pageid:
+ data = self._cw.session.data.get(self._cw.pageid)
+ 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__
+ # 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,
+ check_pageid=False, regid=None):
+ """promote a standard function to an ``AjaxFunction`` appobject.
+
+ All parameters are optional:
+
+ :param selector: a custom selector object if needed, default is ``yes()``
+
+ :param output_type: either None, 'json' or 'xhtml' to customize output
+ content-type. Default is None
+
+ :param check_pageid: whether the function requires a valid `pageid` or not
+ to proceed. Default is False.
+
+ :param regid: a custom __regid__ for the created ``AjaxFunction`` object. Default
+ is to keep the wrapped function name.
+
+ ``ajaxfunc`` can be used both as a standalone decorator:
+
+ .. sourcecode:: python
+
+ @ajaxfunc
+ def my_function(self):
+ return 42
+
+ or as a parametrizable decorator:
+
+ .. sourcecode:: python
+
+ @ajaxfunc(output_type='json')
+ def my_function(self):
+ return 42
+
+ """
+ # if used as a parametrized decorator (e.g. @ajaxfunc(output_type='json'))
+ if implementation is None:
+ def _decorator(func):
+ return _ajaxfunc_factory(func, selector=selector,
+ _output_type=output_type,
+ _check_pageid=check_pageid,
+ regid=regid)
+ return _decorator
+ # else, used as a standalone decorator (i.e. @ajaxfunc)
+ return _ajaxfunc_factory(implementation, selector=selector,
+ _output_type=output_type,
+ _check_pageid=check_pageid, regid=regid)
+
+
+
+###############################################################################
+# Cubicweb remote functions for : #
+# - appobject rendering #
+# - user / page session data management #
+###############################################################################
+@ajaxfunc(output_type='xhtml')
+def view(self):
+ # XXX try to use the page-content template
+ req = self._cw
+ rql = req.form.get('rql')
+ if rql:
+ rset = self._exec(rql)
+ elif 'eid' in req.form:
+ rset = self._cw.eid_rset(req.form['eid'])
+ else:
+ rset = None
+ vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema)
+ try:
+ viewobj = self._cw.vreg['views'].select(vid, req, rset=rset)
+ except NoSelectableObject:
+ vid = req.form.get('fallbackvid', 'noresult')
+ viewobj = self._cw.vreg['views'].select(vid, req, rset=rset)
+ viewobj.set_http_cache_headers()
+ req.validate_cache()
+ return self._call_view(viewobj, paginate=req.form.pop('paginate', False))
+
+
+@ajaxfunc(output_type='xhtml')
+def component(self, compid, rql, registry='components', extraargs=None):
+ if rql:
+ rset = self._exec(rql)
+ else:
+ rset = None
+ # XXX while it sounds good, addition of the try/except below cause pb:
+ # when filtering using facets return an empty rset, the edition box
+ # isn't anymore selectable, as expected. The pb is that with the
+ # try/except below, we see a "an error occurred" message in the ui, while
+ # we don't see it without it. Proper fix would probably be to deal with
+ # this by allowing facet handling code to tell to js_component that such
+ # error is expected and should'nt be reported.
+ #try:
+ comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset,
+ **optional_kwargs(extraargs))
+ #except NoSelectableObject:
+ # raise RemoteCallFailed('unselectable')
+ return self._call_view(comp, **optional_kwargs(extraargs))
+
+@ajaxfunc(output_type='xhtml')
+def render(self, registry, oid, eid=None,
+ selectargs=None, renderargs=None):
+ if eid is not None:
+ rset = self._cw.eid_rset(eid)
+ # XXX set row=0
+ elif self._cw.form.get('rql'):
+ rset = self._cw.execute(self._cw.form['rql'])
+ else:
+ rset = None
+ viewobj = self._cw.vreg[registry].select(oid, self._cw, rset=rset,
+ **optional_kwargs(selectargs))
+ return self._call_view(viewobj, **optional_kwargs(renderargs))
+
+
+@ajaxfunc(output_type='json')
+def i18n(self, msgids):
+ """returns the translation of `msgid`"""
+ return [self._cw._(msgid) for msgid in msgids]
+
+@ajaxfunc(output_type='json')
+def format_date(self, strdate):
+ """returns the formatted date for `msgid`"""
+ date = strptime(strdate, '%Y-%m-%d %H:%M:%S')
+ return self._cw.format_date(date)
+
+@ajaxfunc(output_type='json')
+def external_resource(self, resource):
+ """returns the URL of the external resource named `resource`"""
+ return self._cw.uiprops[resource]
+
+@ajaxfunc(output_type='json', check_pageid=True)
+def user_callback(self, cbname):
+ """execute the previously registered user callback `cbname`.
+
+ If matching callback is not found, return None
+ """
+ page_data = self._cw.session.data.get(self._cw.pageid, {})
+ try:
+ cb = page_data[cbname]
+ except KeyError:
+ self.warning('unable to find user callback %s', cbname)
+ return None
+ return cb(self._cw)
+
+
+@ajaxfunc
+def unregister_user_callback(self, cbname):
+ """unregister user callback `cbname`"""
+ self._cw.unregister_callback(self._cw.pageid, cbname)
+
+@ajaxfunc
+def unload_page_data(self):
+ """remove user's session data associated to current pageid"""
+ self._cw.session.data.pop(self._cw.pageid, None)
+
+@ajaxfunc(output_type='json')
+@deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead")
+def set_cookie(self, cookiename, cookievalue):
+ """generates the Set-Cookie HTTP reponse header corresponding
+ to `cookiename` / `cookievalue`.
+ """
+ cookiename, cookievalue = str(cookiename), str(cookievalue)
+ self._cw.set_cookie(cookiename, cookievalue)
--- a/web/views/ajaxedit.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/ajaxedit.py Tue Jul 10 15:07:52 2012 +0200
@@ -21,7 +21,7 @@
from cubicweb import role
from cubicweb.view import View
-from cubicweb.selectors import match_form_params, match_kwargs
+from cubicweb.predicates import match_form_params, match_kwargs
from cubicweb.web import component, stdmsgs, formwidgets as fw
class AddRelationView(component.EditRelationMixIn, View):
--- a/web/views/autoform.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/autoform.py Tue Jul 10 15:07:52 2012 +0200
@@ -126,18 +126,19 @@
from logilab.mtconverter import xml_escape
from logilab.common.decorators import iclassmethod, cached
from logilab.common.deprecation import deprecated
+from logilab.common.registry import classid
from cubicweb import typed_eid, neg_role, uilib
-from cubicweb.vregistry import classid
from cubicweb.schema import display_name
from cubicweb.view import EntityView
-from cubicweb.selectors import (
+from cubicweb.predicates import (
match_kwargs, match_form_params, non_final_entity,
specified_etype_implements)
from cubicweb.utils import json_dumps
from cubicweb.web import (stdmsgs, uicfg, eid_param,
form as f, formwidgets as fw, formfields as ff)
from cubicweb.web.views import forms
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
_AFS = uicfg.autoform_section
_AFFK = uicfg.autoform_field_kwargs
@@ -437,6 +438,57 @@
execute(rql, {'x': subj, 'y': obj})
+# ajax edition helpers ########################################################
+@ajaxfunc(output_type='xhtml', check_pageid=True)
+def inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx):
+ view = self._cw.vreg['views'].select('inline-creation', self._cw,
+ etype=ttype, rtype=rtype, role=role,
+ peid=peid, petype=petype)
+ return self._call_view(view, i18nctx=i18nctx)
+
+@ajaxfunc(output_type='json')
+def validate_form(self, action, names, values):
+ return self.validate_form(action, names, values)
+
+@ajaxfunc
+def cancel_edition(self, errorurl):
+ """cancelling edition from javascript
+
+ We need to clear associated req's data :
+ - errorurl
+ - pending insertions / deletions
+ """
+ self._cw.cancel_edition(errorurl)
+
+
+def _add_pending(req, eidfrom, rel, eidto, kind):
+ key = 'pending_%s' % kind
+ pendings = req.session.data.setdefault(key, set())
+ pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
+
+def _remove_pending(req, eidfrom, rel, eidto, kind):
+ key = 'pending_%s' % kind
+ pendings = req.session.data[key]
+ pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
+
+@ajaxfunc(output_type='json')
+def remove_pending_insert(self, (eidfrom, rel, eidto)):
+ _remove_pending(self._cw, eidfrom, rel, eidto, 'insert')
+
+@ajaxfunc(output_type='json')
+def add_pending_inserts(self, tripletlist):
+ for eidfrom, rel, eidto in tripletlist:
+ _add_pending(self._cw, eidfrom, rel, eidto, 'insert')
+
+@ajaxfunc(output_type='json')
+def remove_pending_delete(self, (eidfrom, rel, eidto)):
+ _remove_pending(self._cw, eidfrom, rel, eidto, 'delete')
+
+@ajaxfunc(output_type='json')
+def add_pending_delete(self, (eidfrom, rel, eidto)):
+ _add_pending(self._cw, eidfrom, rel, eidto, 'delete')
+
+
class GenericRelationsWidget(fw.FieldWidget):
def render(self, form, field, renderer):
--- a/web/views/basecomponents.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/basecomponents.py Tue Jul 10 15:07:52 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,12 +26,13 @@
_ = unicode
from logilab.mtconverter import xml_escape
+from logilab.common.registry import yes
from logilab.common.deprecation import class_renamed
from rql import parse
-from cubicweb.selectors import (yes, match_form_params, match_context,
- multi_etypes_rset, configuration_values,
- anonymous_user, authenticated_user)
+from cubicweb.predicates import (match_form_params, match_context,
+ multi_etypes_rset, configuration_values,
+ anonymous_user, authenticated_user)
from cubicweb.schema import display_name
from cubicweb.utils import wrap_on_write
from cubicweb.uilib import toggle_action
@@ -76,10 +77,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):
@@ -87,6 +88,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>'
@@ -187,7 +189,7 @@
if msg is None:
msgs = []
if self._cw.cnx:
- srcmsg = self._cw.get_shared_data('sources_error', pop=True)
+ srcmsg = self._cw.get_shared_data('sources_error', pop=True, txdata=True)
if srcmsg:
msgs.append(srcmsg)
reqmsg = self._cw.message # XXX don't call self._cw.message twice
--- a/web/views/basecontrollers.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/basecontrollers.py Tue Jul 10 15:07:52 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.
@@ -22,20 +22,21 @@
__docformat__ = "restructuredtext en"
_ = unicode
-from logilab.common.date import strptime
+from warnings import warn
+
from logilab.common.deprecation import deprecated
from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
- AuthenticationError, typed_eid)
-from cubicweb.utils import UStringIO, json, json_dumps
-from cubicweb.uilib import exc_message
-from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params
-from cubicweb.mail import format_mail
-from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, facet
-from cubicweb.web.controller import Controller
-from cubicweb.web.views import vid_from_rset, formrenderers
+ 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, 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):
"""decorator to sets correct content_type and calls `json_dumps` on
results
@@ -46,6 +47,7 @@
wrapper.__name__ = func.__name__
return wrapper
+@deprecated('[3.15] xhtmlize is deprecated, use AjaxFunction appobjects instead')
def xhtmlize(func):
"""decorator to sets correct content_type and calls `xmlize` on results"""
def wrapper(self, *args, **kwargs):
@@ -56,6 +58,7 @@
wrapper.__name__ = func.__name__
return wrapper
+@deprecated('[3.15] check_pageid is deprecated, use AjaxFunction appobjects instead')
def check_pageid(func):
"""decorator which checks the given pageid is found in the
user's session data
@@ -81,6 +84,15 @@
# 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', '.')
+ raise Redirect(path)
+
class LogoutController(Controller):
__regid__ = 'logout'
@@ -200,7 +212,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:
@@ -208,6 +220,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:
@@ -234,7 +248,7 @@
</script>""" % (domid, callback, errback, jsargs, cbargs)
def publish(self, rset=None):
- self._cw.json_request = True
+ self._cw.ajax_request = True
# XXX unclear why we have a separated controller here vs
# js_validate_form on the json controller
status, args, entity = _validate_form(self._cw, self._cw.vreg)
@@ -242,339 +256,18 @@
self._cw.encoding)
return self.response(domid, status, args, entity)
-def optional_kwargs(extraargs):
- if extraargs is None:
- return {}
- # we receive unicode keys which is not supported by the **syntax
- return dict((str(key), value) for key, value in extraargs.iteritems())
-
class JSonController(Controller):
__regid__ = 'json'
def publish(self, rset=None):
- """call js_* methods. Expected form keys:
-
- :fname: the method name without the js_ prefix
- :args: arguments list (json)
-
- note: it's the responsability of js_* methods to set the correct
- response content type
- """
- self._cw.json_request = True
- try:
- fname = self._cw.form['fname']
- func = getattr(self, 'js_%s' % fname)
- except KeyError:
- raise RemoteCallFailed('no method specified')
- except AttributeError:
- raise RemoteCallFailed('no %s method' % fname)
- # no <arg> attribute means the callback takes no argument
- args = self._cw.form.get('arg', ())
- if not isinstance(args, (list, tuple)):
- args = (args,)
- try:
- args = [json.loads(arg) for arg in args]
- except ValueError, exc:
- self.exception('error while decoding json arguments for js_%s: %s (err: %s)',
- fname, args, exc)
- raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
- try:
- result = func(*args)
- except (RemoteCallFailed, DirectResponse):
- raise
- except Exception, exc:
- self.exception('an exception occurred while calling js_%s(%s): %s',
- fname, args, exc)
- raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
- if result is None:
- return ''
- # get unicode on @htmlize methods, encoded string on @jsonize methods
- elif isinstance(result, unicode):
- return result.encode(self._cw.encoding)
- return result
-
- def _rebuild_posted_form(self, names, values, action=None):
- form = {}
- for name, value in zip(names, values):
- # remove possible __action_xxx inputs
- if name.startswith('__action'):
- if action is None:
- # strip '__action_' to get the actual action name
- action = name[9:]
- continue
- # form.setdefault(name, []).append(value)
- if name in form:
- curvalue = form[name]
- if isinstance(curvalue, list):
- curvalue.append(value)
- else:
- form[name] = [curvalue, value]
- else:
- form[name] = value
- # simulate click on __action_%s button to help the controller
- if action:
- form['__action_%s' % action] = u'whatever'
- return form
-
- def _exec(self, rql, args=None, rocheck=True):
- """json mode: execute RQL and return resultset as json"""
- rql = rql.strip()
- if rql.startswith('rql:'):
- rql = rql[4:]
- if rocheck:
- self._cw.ensure_ro_rql(rql)
- try:
- return self._cw.execute(rql, args)
- except Exception, ex:
- self.exception("error in _exec(rql=%s): %s", rql, ex)
- return None
- return None
-
- def _call_view(self, view, paginate=False, **kwargs):
- divid = self._cw.form.get('divid')
- # we need to call pagination before with the stream set
- try:
- stream = view.set_stream()
- except AttributeError:
- stream = UStringIO()
- kwargs['w'] = stream.write
- assert not paginate
- if divid == 'pageContent':
- # ensure divid isn't reused by the view (e.g. table view)
- del self._cw.form['divid']
- # mimick main template behaviour
- stream.write(u'<div id="pageContent">')
- vtitle = self._cw.form.get('vtitle')
- if vtitle:
- stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
- paginate = True
- nav_html = UStringIO()
- if paginate and not view.handle_pagination:
- view.paginate(w=nav_html.write)
- stream.write(nav_html.getvalue())
- if divid == 'pageContent':
- stream.write(u'<div id="contentmain">')
- view.render(**kwargs)
- extresources = self._cw.html_headers.getvalue(skiphead=True)
- if extresources:
- stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
- stream.write(extresources)
- stream.write(u'</div>\n')
- if divid == 'pageContent':
- stream.write(u'</div>%s</div>' % nav_html.getvalue())
- return stream.getvalue()
-
- @xhtmlize
- def js_view(self):
- # XXX try to use the page-content template
- req = self._cw
- rql = req.form.get('rql')
- if rql:
- rset = self._exec(rql)
- elif 'eid' in req.form:
- rset = self._cw.eid_rset(req.form['eid'])
- else:
- rset = None
- vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema)
- try:
- view = self._cw.vreg['views'].select(vid, req, rset=rset)
- except NoSelectableObject:
- vid = req.form.get('fallbackvid', 'noresult')
- view = self._cw.vreg['views'].select(vid, req, rset=rset)
- self.validate_cache(view)
- return self._call_view(view, paginate=req.form.pop('paginate', False))
-
- @xhtmlize
- def js_prop_widget(self, propkey, varname, tabindex=None):
- """specific method for CWProperty handling"""
- entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
- entity.eid = varname
- entity['pkey'] = propkey
- form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
- form.build_context()
- vfield = form.field_by_name('value')
- renderer = formrenderers.FormRenderer(self._cw)
- return vfield.render(form, renderer, tabindex=tabindex) \
- + renderer.render_help(form, vfield)
-
- @xhtmlize
- def js_component(self, compid, rql, registry='components', extraargs=None):
- if rql:
- rset = self._exec(rql)
- else:
- rset = None
- # XXX while it sounds good, addition of the try/except below cause pb:
- # when filtering using facets return an empty rset, the edition box
- # isn't anymore selectable, as expected. The pb is that with the
- # try/except below, we see a "an error occurred" message in the ui, while
- # we don't see it without it. Proper fix would probably be to deal with
- # this by allowing facet handling code to tell to js_component that such
- # error is expected and should'nt be reported.
- #try:
- comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset,
- **optional_kwargs(extraargs))
- #except NoSelectableObject:
- # raise RemoteCallFailed('unselectable')
- return self._call_view(comp, **optional_kwargs(extraargs))
-
- @xhtmlize
- def js_render(self, registry, oid, eid=None,
- selectargs=None, renderargs=None):
- if eid is not None:
- rset = self._cw.eid_rset(eid)
- # XXX set row=0
- elif self._cw.form.get('rql'):
- rset = self._cw.execute(self._cw.form['rql'])
- else:
- rset = None
- view = self._cw.vreg[registry].select(oid, self._cw, rset=rset,
- **optional_kwargs(selectargs))
- return self._call_view(view, **optional_kwargs(renderargs))
-
- @check_pageid
- @xhtmlize
- def js_inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx):
- view = self._cw.vreg['views'].select('inline-creation', self._cw,
- etype=ttype, rtype=rtype, role=role,
- peid=peid, petype=petype)
- return self._call_view(view, i18nctx=i18nctx)
-
- @jsonize
- def js_validate_form(self, action, names, values):
- return self.validate_form(action, names, values)
-
- def validate_form(self, action, names, values):
- self._cw.form = self._rebuild_posted_form(names, values, action)
- return _validate_form(self._cw, self._cw.vreg)
-
- @xhtmlize
- def js_reledit_form(self):
- req = self._cw
- args = dict((x, req.form[x])
- for x in ('formid', 'rtype', 'role', 'reload', 'action'))
- rset = req.eid_rset(typed_eid(self._cw.form['eid']))
- try:
- args['reload'] = json.loads(args['reload'])
- except ValueError: # not true/false, an absolute url
- assert args['reload'].startswith('http')
- view = req.vreg['views'].select('reledit', req, rset=rset, rtype=args['rtype'])
- return self._call_view(view, **args)
-
- @jsonize
- def js_i18n(self, msgids):
- """returns the translation of `msgid`"""
- return [self._cw._(msgid) for msgid in msgids]
-
- @jsonize
- def js_format_date(self, strdate):
- """returns the formatted date for `msgid`"""
- date = strptime(strdate, '%Y-%m-%d %H:%M:%S')
- return self._cw.format_date(date)
-
- @jsonize
- def js_external_resource(self, resource):
- """returns the URL of the external resource named `resource`"""
- return self._cw.uiprops[resource]
-
- @check_pageid
- @jsonize
- def js_user_callback(self, cbname):
- page_data = self._cw.session.data.get(self._cw.pageid, {})
- try:
- cb = page_data[cbname]
- except KeyError:
- return None
- return cb(self._cw)
-
- @jsonize
- def js_filter_build_rql(self, names, values):
- form = self._rebuild_posted_form(names, values)
- self._cw.form = form
- builder = facet.FilterRQLBuilder(self._cw)
- return builder.build_rql()
-
- @jsonize
- def js_filter_select_content(self, facetids, rql, mainvar):
- # Union unsupported yet
- select = self._cw.vreg.parse(self._cw, rql).children[0]
- filtered_variable = facet.get_filtered_variable(select, mainvar)
- facet.prepare_select(select, filtered_variable)
- update_map = {}
- for fid in facetids:
- fobj = facet.get_facet(self._cw, fid, select, filtered_variable)
- update_map[fid] = fobj.possible_values()
- return update_map
-
- def js_unregister_user_callback(self, cbname):
- self._cw.unregister_callback(self._cw.pageid, cbname)
-
- def js_unload_page_data(self):
- self._cw.session.data.pop(self._cw.pageid, None)
-
- def js_cancel_edition(self, errorurl):
- """cancelling edition from javascript
-
- We need to clear associated req's data :
- - errorurl
- - pending insertions / deletions
- """
- self._cw.cancel_edition(errorurl)
-
- def js_delete_bookmark(self, beid):
- rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s'
- self._cw.execute(rql, {'b': typed_eid(beid), 'u' : self._cw.user.eid})
-
- def js_node_clicked(self, treeid, nodeeid):
- """add/remove eid in treestate cookie"""
- from cubicweb.web.views.treeview import treecookiename
- cookies = self._cw.get_cookie()
- statename = treecookiename(treeid)
- treestate = cookies.get(statename)
- if treestate is None:
- self._cw.set_cookie(statename, nodeeid)
- else:
- marked = set(filter(None, treestate.value.split(':')))
- if nodeeid in marked:
- marked.remove(nodeeid)
- else:
- marked.add(nodeeid)
- self._cw.set_cookie(statename, ':'.join(marked))
-
- @jsonize
- @deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead")
- def js_set_cookie(self, cookiename, cookievalue):
- cookiename, cookievalue = str(cookiename), str(cookievalue)
- self._cw.set_cookie(cookiename, cookievalue)
-
- # relations edition stuff ##################################################
-
- def _add_pending(self, eidfrom, rel, eidto, kind):
- key = 'pending_%s' % kind
- pendings = self._cw.session.data.setdefault(key, set())
- pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
-
- def _remove_pending(self, eidfrom, rel, eidto, kind):
- key = 'pending_%s' % kind
- pendings = self._cw.session.data[key]
- pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
-
- def js_remove_pending_insert(self, (eidfrom, rel, eidto)):
- self._remove_pending(eidfrom, rel, eidto, 'insert')
-
- def js_add_pending_inserts(self, tripletlist):
- for eidfrom, rel, eidto in tripletlist:
- self._add_pending(eidfrom, rel, eidto, 'insert')
-
- def js_remove_pending_delete(self, (eidfrom, rel, eidto)):
- self._remove_pending(eidfrom, rel, eidto, 'delete')
-
- def js_add_pending_delete(self, (eidfrom, rel, eidto)):
- self._add_pending(eidfrom, rel, eidto, 'delete')
+ warn('[3.15] JSONController is deprecated, use AjaxController instead',
+ DeprecationWarning)
+ ajax_controller = self._cw.vreg['controllers'].select('ajax', self._cw, appli=self.appli)
+ return ajax_controller.publish(rset)
# XXX move to massmailing
-
class MailBugReportController(Controller):
__regid__ = 'reportbug'
__select__ = match_form_params('description')
@@ -593,17 +286,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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/basetemplates.py Tue Jul 10 15:07:52 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.
@@ -22,9 +22,10 @@
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.appobject import objectify_selector
-from cubicweb.selectors import match_kwargs, no_cnx, anonymous_user
+from cubicweb.predicates import match_kwargs, no_cnx, anonymous_user
from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW, StartupView
from cubicweb.utils import UStringIO
from cubicweb.schema import display_name
@@ -84,7 +85,7 @@
self.w(u'<h2>%s</h2>' % msg)
-@objectify_selector
+@objectify_predicate
def templatable_view(cls, req, rset, *args, **kwargs):
view = kwargs.pop('view', None)
if view is None:
@@ -328,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)
@@ -417,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 exist
+ 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):
@@ -482,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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/baseviews.py Tue Jul 10 15:07:52 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.
@@ -84,9 +84,10 @@
from rql import nodes
from logilab.mtconverter import TransformError, xml_escape
+from logilab.common.registry import yes
from cubicweb import NoSelectableObject, tags
-from cubicweb.selectors import yes, empty_rset, one_etype_rset, match_kwargs
+from cubicweb.predicates import empty_rset, one_etype_rset, match_kwargs
from cubicweb.schema import display_name
from cubicweb.view import EntityView, AnyRsetView, View
from cubicweb.uilib import cut
@@ -157,7 +158,7 @@
""":__regid__: *incontext*
This view is used whenthe entity should be considered as displayed in its
- context. By default it produces the result of `textincontext` wrapped in a
+ context. By default it produces the result of ``entity.dc_title()`` wrapped in a
link leading to the primary view of the entity.
"""
__regid__ = 'incontext'
@@ -165,30 +166,25 @@
def cell_call(self, row, col):
entity = self.cw_rset.get_entity(row, col)
desc = cut(entity.dc_description(), 50)
- self.w(u'<a href="%s" title="%s">' % (
- xml_escape(entity.absolute_url()), xml_escape(desc)))
- self.w(xml_escape(self._cw.view('textincontext', self.cw_rset,
- row=row, col=col)))
- self.w(u'</a>')
-
+ self.w(u'<a href="%s" title="%s">%s</a>' % (
+ xml_escape(entity.absolute_url()), xml_escape(desc),
+ xml_escape(entity.dc_title())))
class OutOfContextView(EntityView):
""":__regid__: *outofcontext*
This view is used when the entity should be considered as displayed out of
- its context. By default it produces the result of `textoutofcontext` wrapped
- in a link leading to the primary view of the entity.
+ 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'
def cell_call(self, row, col):
entity = self.cw_rset.get_entity(row, col)
desc = cut(entity.dc_description(), 50)
- self.w(u'<a href="%s" title="%s">' % (
- xml_escape(entity.absolute_url()), xml_escape(desc)))
- self.w(xml_escape(self._cw.view('textoutofcontext', self.cw_rset,
- row=row, col=col)))
- self.w(u'</a>')
+ self.w(u'<a href="%s" title="%s">%s</a>' % (
+ xml_escape(entity.absolute_url()), xml_escape(desc),
+ xml_escape(entity.dc_long_title())))
class OneLineView(EntityView):
@@ -205,9 +201,12 @@
"""the one line view for an entity: linked text view
"""
entity = self.cw_rset.get_entity(row, col)
- self.w(u'<a href="%s">' % xml_escape(entity.absolute_url()))
- self.w(xml_escape(self._cw.view('text', self.cw_rset, row=row, col=col)))
- self.w(u'</a>')
+ desc = cut(entity.dc_description(), 50)
+ title = cut(entity.dc_title(),
+ self._cw.property_value('navigation.short-line-size'))
+ self.w(u'<a href="%s" title="%s">%s</a>' % (
+ xml_escape(entity.absolute_url()), xml_escape(desc),
+ xml_escape(title)))
# text views ###################################################################
--- a/web/views/bookmark.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/bookmark.py Tue Jul 10 15:07:52 2012 +0200
@@ -22,11 +22,12 @@
from logilab.mtconverter import xml_escape
-from cubicweb import Unauthorized
-from cubicweb.selectors import is_instance, one_line_rset
+from cubicweb import Unauthorized, typed_eid
+from cubicweb.predicates import is_instance, one_line_rset
from cubicweb.web import (action, component, uicfg, htmlwidgets,
formwidgets as fw)
from cubicweb.web.views import primary
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
_abaa = uicfg.actionbox_appearsin_addmenu
_abaa.tag_subject_of(('*', 'bookmarked_by', '*'), False)
@@ -133,3 +134,8 @@
menu.append(self.link(req._('pick existing bookmarks'), url))
self.append(menu)
self.render_items(w)
+
+@ajaxfunc
+def delete_bookmark(self, beid):
+ rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s'
+ self._cw.execute(rql, {'b': typed_eid(beid), 'u' : self._cw.user.eid})
--- a/web/views/boxes.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/boxes.py Tue Jul 10 15:07:52 2012 +0200
@@ -36,7 +36,7 @@
from logilab.common.deprecation import class_deprecated
from cubicweb import Unauthorized
-from cubicweb.selectors import (match_user_groups, match_kwargs,
+from cubicweb.predicates import (match_user_groups, match_kwargs,
non_final_entity, nonempty_rset,
match_context, contextual)
from cubicweb.utils import wrap_on_write
@@ -54,7 +54,6 @@
change state, add related entities...
"""
__regid__ = 'edit_box'
- __select__ = component.CtxComponent.__select__ & non_final_entity()
title = _('actions')
order = 2
@@ -136,13 +135,13 @@
title = _('search')
order = 0
- formdef = u"""<form action="%s">
-<table id="tsearch"><tr><td>
-<input id="norql" type="text" accesskey="q" tabindex="%s" title="search text" value="%s" name="rql" />
+ formdef = u"""<form action="%(action)s">
+<table id="%(id)s"><tr><td>
+<input class="norql" type="text" accesskey="q" tabindex="%(tabindex1)s" title="search text" value="%(value)s" name="rql" />
<input type="hidden" name="__fromsearchbox" value="1" />
<input type="hidden" name="subvid" value="tsearch" />
</td><td>
-<input tabindex="%s" type="submit" id="rqlboxsubmit" class="rqlsubmit" value="" />
+<input tabindex="%(tabindex2)s" type="submit" class="rqlsubmit" value="" />
</td></tr></table>
</form>"""
@@ -155,8 +154,13 @@
rql = self._cw.form.get('rql', '')
else:
rql = ''
- w(self.formdef % (self._cw.build_url('view'), self._cw.next_tabindex(),
- xml_escape(rql), self._cw.next_tabindex()))
+ tabidx1 = self._cw.next_tabindex()
+ tabidx2 = self._cw.next_tabindex()
+ w(self.formdef % {'action': self._cw.build_url('view'),
+ 'value': xml_escape(rql),
+ 'id': self.cw_extra_kwargs.get('domid', 'tsearch'),
+ 'tabindex1': tabidx1,
+ 'tabindex2': tabidx2})
# boxes disabled by default ###################################################
--- a/web/views/calendar.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/calendar.py Tue Jul 10 15:07:52 2012 +0200
@@ -28,7 +28,7 @@
from cubicweb.utils import json_dumps, make_uid
from cubicweb.interfaces import ICalendarable
-from cubicweb.selectors import implements, adaptable
+from cubicweb.predicates import implements, adaptable
from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
# useful constants & functions ################################################
--- a/web/views/cwproperties.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/cwproperties.py Tue Jul 10 15:07:52 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.
@@ -25,9 +25,8 @@
from logilab.common.decorators import cached
from cubicweb import UnknownProperty
-from cubicweb.selectors import (one_line_rset, none_rset, is_instance,
- match_user_groups, objectify_selector,
- logged_user_in_rset)
+from cubicweb.predicates import (one_line_rset, none_rset, is_instance,
+ match_user_groups, logged_user_in_rset)
from cubicweb.view import StartupView
from cubicweb.web import uicfg, stdmsgs
from cubicweb.web.form import FormViewMixIn
@@ -35,6 +34,7 @@
from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton,
FieldWidget)
from cubicweb.web.views import primary, formrenderers, editcontroller
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden')
@@ -419,6 +419,20 @@
"""
return 'view', {}
+
+@ajaxfunc(output_type='xhtml')
+def prop_widget(self, propkey, varname, tabindex=None):
+ """specific method for CWProperty handling"""
+ entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
+ entity.eid = varname
+ entity['pkey'] = propkey
+ form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
+ form.build_context()
+ vfield = form.field_by_name('value')
+ renderer = formrenderers.FormRenderer(self._cw)
+ return vfield.render(form, renderer, tabindex=tabindex) \
+ + renderer.render_help(form, vfield)
+
_afs = uicfg.autoform_section
_afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden')
_afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden')
--- a/web/views/cwsources.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/cwsources.py Tue Jul 10 15:07:52 2012 +0200
@@ -29,7 +29,7 @@
from cubicweb import Unauthorized, tags
from cubicweb.utils import make_uid
-from cubicweb.selectors import (is_instance, score_entity, has_related_entities,
+from cubicweb.predicates import (is_instance, score_entity, has_related_entities,
match_user_groups, match_kwargs, match_view)
from cubicweb.view import EntityView, StartupView
from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name
--- a/web/views/cwuser.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/cwuser.py Tue Jul 10 15:07:52 2012 +0200
@@ -26,7 +26,7 @@
from cubicweb import tags
from cubicweb.schema import display_name
-from cubicweb.selectors import one_line_rset, is_instance, match_user_groups
+from cubicweb.predicates import one_line_rset, is_instance, match_user_groups
from cubicweb.view import EntityView, StartupView
from cubicweb.web import action, uicfg, formwidgets
from cubicweb.web.views import tabs, tableview, actions, add_etype_button
--- a/web/views/debug.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/debug.py Tue Jul 10 15:07:52 2012 +0200
@@ -25,7 +25,7 @@
from logilab.mtconverter import xml_escape
from cubicweb import BadConnectionId
-from cubicweb.selectors import none_rset, match_user_groups
+from cubicweb.predicates import none_rset, match_user_groups
from cubicweb.view import StartupView
from cubicweb.web.views import actions, tabs
--- a/web/views/editcontroller.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/editcontroller.py Tue Jul 10 15:07:52 2012 +0200
@@ -27,7 +27,7 @@
from cubicweb import Binary, ValidationError, typed_eid
from cubicweb.view import EntityAdapter, implements_adapter_compat
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
ProcessFormError)
from cubicweb.web.views import basecontrollers, autoform
@@ -161,7 +161,7 @@
neweid = entity.eid
except ValidationError, ex:
self._to_create[eid] = ex.entity
- if self._cw.json_request: # XXX (syt) why?
+ if self._cw.ajax_request: # XXX (syt) why?
ex.entity = eid
raise
self._to_create[eid] = neweid
--- a/web/views/editforms.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/editforms.py Tue Jul 10 15:07:52 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.
@@ -26,11 +26,12 @@
from logilab.mtconverter import xml_escape
from logilab.common.decorators import cached
+from logilab.common.registry import yes
from logilab.common.deprecation import class_moved
from cubicweb import tags
-from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity,
- specified_etype_implements, is_instance, yes)
+from cubicweb.predicates import (match_kwargs, one_line_rset, non_final_entity,
+ specified_etype_implements, is_instance)
from cubicweb.view import EntityView
from cubicweb.schema import display_name
from cubicweb.web import uicfg, stdmsgs, eid_param, \
--- a/web/views/editviews.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/editviews.py Tue Jul 10 15:07:52 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.
@@ -25,8 +25,8 @@
from cubicweb import typed_eid
from cubicweb.view import EntityView, StartupView
-from cubicweb.selectors import (one_line_rset, non_final_entity,
- match_search_state)
+from cubicweb.predicates import (one_line_rset, non_final_entity,
+ match_search_state)
from cubicweb.web import httpcache
from cubicweb.web.views import baseviews, linksearch_select_url
--- a/web/views/emailaddress.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/emailaddress.py Tue Jul 10 15:07:52 2012 +0200
@@ -22,7 +22,7 @@
from logilab.mtconverter import xml_escape
from cubicweb.schema import display_name
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb import Unauthorized
from cubicweb.web import uicfg
from cubicweb.web.views import baseviews, primary, ibreadcrumbs
--- a/web/views/embedding.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/embedding.py Tue Jul 10 15:07:52 2012 +0200
@@ -29,7 +29,7 @@
from logilab.mtconverter import guess_encoding
-from cubicweb.selectors import (one_line_rset, score_entity, implements,
+from cubicweb.predicates import (one_line_rset, score_entity, implements,
adaptable, match_search_state)
from cubicweb.interfaces import IEmbedable
from cubicweb.view import NOINDEX, NOFOLLOW, EntityAdapter, implements_adapter_compat
--- a/web/views/facets.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/facets.py Tue Jul 10 15:07:52 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,14 +24,15 @@
from logilab.mtconverter import xml_escape
from logilab.common.decorators import cachedproperty
+from logilab.common.registry import objectify_predicate, yes
-from cubicweb.appobject import objectify_selector
-from cubicweb.selectors import (non_final_entity, multi_lines_rset,
- match_context_prop, yes, relation_possible)
+from cubicweb.predicates import (non_final_entity, multi_lines_rset,
+ match_context_prop, relation_possible)
from cubicweb.utils import json_dumps
from cubicweb.uilib import css_em_num_value
from cubicweb.view import AnyRsetView
from cubicweb.web import component, facet as facetbase
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
def facets(req, rset, context, mainvar=None, **kwargs):
"""return the base rql and a list of widgets for facets applying to the
@@ -81,7 +82,7 @@
return baserql, [wdg for facet, wdg in wdgs if wdg is not None]
-@objectify_selector
+@objectify_predicate
def contextview_selector(cls, req, rset=None, row=None, col=None, view=None,
**kwargs):
if view:
@@ -96,7 +97,7 @@
return len(wdgs)
return 0
-@objectify_selector
+@objectify_predicate
def has_facets(cls, req, rset=None, **kwargs):
if rset is None or rset.rowcount < 2:
return 0
@@ -313,6 +314,28 @@
w(u'</div>')
w(u'</div>\n')
+# python-ajax remote functions used by facet widgets #########################
+
+@ajaxfunc(output_type='json')
+def filter_build_rql(self, names, values):
+ form = self._rebuild_posted_form(names, values)
+ self._cw.form = form
+ builder = facetbase.FilterRQLBuilder(self._cw)
+ return builder.build_rql()
+
+@ajaxfunc(output_type='json')
+def filter_select_content(self, facetids, rql, mainvar):
+ # Union unsupported yet
+ select = self._cw.vreg.parse(self._cw, rql).children[0]
+ filtered_variable = facetbase.get_filtered_variable(select, mainvar)
+ facetbase.prepare_select(select, filtered_variable)
+ update_map = {}
+ for fid in facetids:
+ fobj = facetbase.get_facet(self._cw, fid, select, filtered_variable)
+ update_map[fid] = fobj.possible_values()
+ return update_map
+
+
# facets ######################################################################
--- a/web/views/formrenderers.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/formrenderers.py Tue Jul 10 15:07:52 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,10 +38,11 @@
from warnings import warn
from logilab.mtconverter import xml_escape
+from logilab.common.registry import yes
from cubicweb import tags, uilib
from cubicweb.appobject import AppObject
-from cubicweb.selectors import is_instance, yes
+from cubicweb.predicates import is_instance
from cubicweb.utils import json_dumps, support_args
from cubicweb.web import eid_param, formwidgets as fwdgs
@@ -116,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:
@@ -491,17 +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:
@@ -510,22 +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
- w(u'<div class="iformTitle">')
- self.render_title(w, form, values)
- w(u'</div>')
- # 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/forms.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/forms.py Tue Jul 10 15:07:52 2012 +0200
@@ -54,7 +54,7 @@
from cubicweb import ValidationError, typed_eid
from cubicweb.utils import support_args
-from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset
+from cubicweb.predicates import non_final_entity, match_kwargs, one_line_rset
from cubicweb.web import RequestError, ProcessFormError
from cubicweb.web import uicfg, form, formwidgets as fwdgs
from cubicweb.web.formfields import guess_field
@@ -406,7 +406,7 @@
return self.force_session_key
# XXX if this is a json request, suppose we should redirect to the
# entity primary view
- if self._cw.json_request and self.edited_entity.has_eid():
+ if self._cw.ajax_request and self.edited_entity.has_eid():
return '%s#%s' % (self.edited_entity.absolute_url(), self.domid)
# XXX we should not consider some url parameters that may lead to
# different url after a validation error
--- a/web/views/ibreadcrumbs.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/ibreadcrumbs.py Tue Jul 10 15:07:52 2012 +0200
@@ -27,7 +27,7 @@
#from cubicweb.interfaces import IBreadCrumbs
from cubicweb import tags, uilib
from cubicweb.entity import Entity
-from cubicweb.selectors import (is_instance, one_line_rset, adaptable,
+from cubicweb.predicates import (is_instance, one_line_rset, adaptable,
one_etype_rset, multi_lines_rset, any_rset,
match_form_params)
from cubicweb.view import EntityView, EntityAdapter
--- a/web/views/idownloadable.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/idownloadable.py Tue Jul 10 15:07:52 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,8 +27,8 @@
from cubicweb import tags
from cubicweb.view import EntityView
-from cubicweb.selectors import (one_line_rset, is_instance, match_context_prop,
- adaptable, has_mimetype)
+from cubicweb.predicates import (one_line_rset, is_instance, match_context_prop,
+ adaptable, has_mimetype)
from cubicweb.mttransforms import ENGINE
from cubicweb.web import component, httpcache
from cubicweb.web.views import primary, baseviews
--- a/web/views/igeocodable.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/igeocodable.py Tue Jul 10 15:07:52 2012 +0200
@@ -21,7 +21,7 @@
from cubicweb.interfaces import IGeocodable
from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
-from cubicweb.selectors import implements, adaptable
+from cubicweb.predicates import implements, adaptable
from cubicweb.utils import json_dumps
class IGeocodableAdapter(EntityAdapter):
--- a/web/views/iprogress.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/iprogress.py Tue Jul 10 15:07:52 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,7 +26,7 @@
from logilab.mtconverter import xml_escape
from cubicweb.utils import make_uid
-from cubicweb.selectors import adaptable
+from cubicweb.predicates import adaptable
from cubicweb.schema import display_name
from cubicweb.view import EntityView
from cubicweb.web.views.tableview import EntityAttributesTableView
--- a/web/views/isioc.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/isioc.py Tue Jul 10 15:07:52 2012 +0200
@@ -26,7 +26,7 @@
from logilab.mtconverter import xml_escape
from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
-from cubicweb.selectors import implements, adaptable
+from cubicweb.predicates import implements, adaptable
from cubicweb.interfaces import ISiocItem, ISiocContainer
--- a/web/views/management.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/management.py Tue Jul 10 15:07:52 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.
@@ -21,8 +21,9 @@
_ = unicode
from logilab.mtconverter import xml_escape
+from logilab.common.registry import yes
-from cubicweb.selectors import yes, none_rset, match_user_groups, authenticated_user
+from cubicweb.predicates import none_rset, match_user_groups, authenticated_user
from cubicweb.view import AnyRsetView, StartupView, EntityView, View
from cubicweb.uilib import html_traceback, rest_traceback, exc_message
from cubicweb.web import formwidgets as wdgs
--- a/web/views/massmailing.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/massmailing.py Tue Jul 10 15:07:52 2012 +0200
@@ -22,7 +22,7 @@
import operator
-from cubicweb.selectors import (is_instance, authenticated_user,
+from cubicweb.predicates import (is_instance, authenticated_user,
adaptable, match_form_params)
from cubicweb.view import EntityView
from cubicweb.web import (Redirect, stdmsgs, controller, action,
--- a/web/views/navigation.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/navigation.py Tue Jul 10 15:07:52 2012 +0200
@@ -55,7 +55,7 @@
from logilab.mtconverter import xml_escape
from logilab.common.deprecation import deprecated
-from cubicweb.selectors import (paginated_rset, sorted_rset,
+from cubicweb.predicates import (paginated_rset, sorted_rset,
adaptable, implements)
from cubicweb.uilib import cut
from cubicweb.view import EntityAdapter, implements_adapter_compat
--- a/web/views/owl.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/owl.py Tue Jul 10 15:07:52 2012 +0200
@@ -24,7 +24,7 @@
from logilab.mtconverter import TransformError, xml_escape
from cubicweb.view import StartupView, EntityView
-from cubicweb.selectors import none_rset, match_view
+from cubicweb.predicates import none_rset, match_view
from cubicweb.web.action import Action
from cubicweb.web.views import schema
--- a/web/views/plots.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/plots.py Tue Jul 10 15:07:52 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.
@@ -22,14 +22,14 @@
from logilab.common.date import datetime2ticks
from logilab.common.deprecation import class_deprecated
+from logilab.common.registry import objectify_predicate
from logilab.mtconverter import xml_escape
from cubicweb.utils import UStringIO, json_dumps
-from cubicweb.appobject import objectify_selector
-from cubicweb.selectors import multi_columns_rset
+from cubicweb.predicates import multi_columns_rset
from cubicweb.web.views import baseviews
-@objectify_selector
+@objectify_predicate
def all_columns_are_numbers(cls, req, rset=None, *args, **kwargs):
"""accept result set with at least one line and two columns of result
all columns after second must be of numerical types"""
@@ -38,14 +38,14 @@
return 0
return 1
-@objectify_selector
+@objectify_predicate
def second_column_is_number(cls, req, rset=None, *args, **kwargs):
etype = rset.description[0][1]
if etype not in ('Int', 'BigInt', 'Float'):
return 0
return 1
-@objectify_selector
+@objectify_predicate
def columns_are_date_then_numbers(cls, req, rset=None, *args, **kwargs):
etypes = rset.description[0]
if etypes[0] not in ('Date', 'Datetime', 'TZDatetime'):
--- a/web/views/primary.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/primary.py Tue Jul 10 15:07:52 2012 +0200
@@ -47,7 +47,7 @@
from cubicweb import Unauthorized, NoSelectableObject
from cubicweb.utils import support_args
-from cubicweb.selectors import match_kwargs, match_context
+from cubicweb.predicates import match_kwargs, match_context
from cubicweb.view import EntityView
from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name
from cubicweb.web import uicfg, component
--- a/web/views/pyviews.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/pyviews.py Tue Jul 10 15:07:52 2012 +0200
@@ -20,7 +20,7 @@
__docformat__ = "restructuredtext en"
from cubicweb.view import View
-from cubicweb.selectors import match_kwargs
+from cubicweb.predicates import match_kwargs
from cubicweb.web.views import tableview
--- a/web/views/reledit.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/reledit.py Tue Jul 10 15:07:52 2012 +0200
@@ -29,14 +29,15 @@
from logilab.common.deprecation import deprecated, class_renamed
from logilab.common.decorators import cached
-from cubicweb import neg_role
+from cubicweb import neg_role, typed_eid
from cubicweb.schema import display_name
-from cubicweb.utils import json_dumps
-from cubicweb.selectors import non_final_entity, match_kwargs
+from cubicweb.utils import json, json_dumps
+from cubicweb.predicates import non_final_entity, match_kwargs
from cubicweb.view import EntityView
from cubicweb.web import uicfg, stdmsgs
from cubicweb.web.form import FieldNotFound
from cubicweb.web.formwidgets import Button, SubmitButton
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
class _DummyForm(object):
__slots__ = ('event_args',)
@@ -394,3 +395,18 @@
ClickAndEditFormView = class_renamed('ClickAndEditFormView', AutoClickAndEditFormView)
+
+
+@ajaxfunc(output_type='xhtml')
+def reledit_form(self):
+ req = self._cw
+ args = dict((x, req.form[x])
+ for x in ('formid', 'rtype', 'role', 'reload', 'action'))
+ rset = req.eid_rset(typed_eid(self._cw.form['eid']))
+ try:
+ args['reload'] = json.loads(args['reload'])
+ except ValueError: # not true/false, an absolute url
+ assert args['reload'].startswith('http')
+ view = req.vreg['views'].select('reledit', req, rset=rset, rtype=args['rtype'])
+ return self._call_view(view, **args)
+
--- a/web/views/schema.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/schema.py Tue Jul 10 15:07:52 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,12 +27,13 @@
from logilab.common.graph import GraphGenerator, DotBackend
from logilab.common.ureports import Section, Table
+from logilab.common.registry import yes
from logilab.mtconverter import xml_escape
from yams import BASE_TYPES, schema2dot as s2d
from yams.buildobjs import DEFAULT_ATTRPERMS
-from cubicweb.selectors import (is_instance, match_user_groups, match_kwargs,
- has_related_entities, authenticated_user, yes)
+from cubicweb.predicates import (is_instance, match_user_groups, match_kwargs,
+ has_related_entities, authenticated_user)
from cubicweb.schema import (META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES,
WORKFLOW_TYPES, INTERNAL_TYPES)
from cubicweb.utils import make_uid
--- a/web/views/sessions.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/sessions.py Tue Jul 10 15:07:52 2012 +0200
@@ -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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/startup.py Tue Jul 10 15:07:52 2012 +0200
@@ -29,7 +29,7 @@
from logilab.mtconverter import xml_escape
from cubicweb.view import StartupView
-from cubicweb.selectors import match_user_groups, is_instance
+from cubicweb.predicates import match_user_groups, is_instance
from cubicweb.schema import display_name
from cubicweb.web import uicfg, httpcache
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/staticcontrollers.py Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/tableview.py Tue Jul 10 15:07:52 2012 +0200
@@ -70,9 +70,10 @@
from logilab.mtconverter import xml_escape
from logilab.common.decorators import cachedproperty
from logilab.common.deprecation import class_deprecated
+from logilab.common.registry import yes
from cubicweb import NoSelectableObject, tags
-from cubicweb.selectors import yes, nonempty_rset, match_kwargs, objectify_selector
+from cubicweb.predicates import nonempty_rset, match_kwargs, objectify_predicate
from cubicweb.schema import display_name
from cubicweb.utils import make_uid, js_dumps, JSString, UStringIO
from cubicweb.uilib import toggle_action, limitsize, htmlescape, sgml_attributes, domid
@@ -82,7 +83,7 @@
PopupBoxMenu)
-@objectify_selector
+@objectify_predicate
def unreloadable_table(cls, req, rset=None,
displaycols=None, headers=None, cellvids=None,
paginate=False, displayactions=False, displayfilter=False,
@@ -458,12 +459,9 @@
# layout callbacks #########################################################
def facets_form(self, **kwargs):# XXX extracted from jqplot cube
- try:
- return self._cw.vreg['views'].select(
- 'facet.filtertable', self._cw, rset=self.cw_rset, view=self,
- **kwargs)
- except NoSelectableObject:
- return None
+ return self._cw.vreg['views'].select_or_none(
+ 'facet.filtertable', self._cw, rset=self.cw_rset, view=self,
+ **kwargs)
@cachedproperty
def domid(self):
@@ -866,7 +864,7 @@
class EntityTableView(TableMixIn, EntityView):
"""This abstract table view is designed to be used with an
- :class:`is_instance()` or :class:`adaptable` selector, hence doesn't depend
+ :class:`is_instance()` or :class:`adaptable` predicate, hence doesn't depend
the result set shape as the :class:`TableView` does.
It will display columns that should be defined using the `columns` class
--- a/web/views/tabs.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/tabs.py Tue Jul 10 15:07:52 2012 +0200
@@ -25,7 +25,7 @@
from cubicweb import NoSelectableObject, role
from cubicweb import tags, uilib, utils
-from cubicweb.selectors import partial_has_related_entities
+from cubicweb.predicates import partial_has_related_entities
from cubicweb.view import EntityView
from cubicweb.web.views import primary
@@ -47,12 +47,20 @@
"""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
elif eid:
- urlparams['rql'] = uilib.rql_for_eid(eid)
+ urlparams['eid'] = eid
elif rset:
urlparams['rql'] = rset.printable_rql()
if tabid is None:
--- a/web/views/timeline.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/timeline.py Tue Jul 10 15:07:52 2012 +0200
@@ -25,7 +25,7 @@
from logilab.mtconverter import xml_escape
from logilab.common.date import ustrftime
-from cubicweb.selectors import adaptable
+from cubicweb.predicates import adaptable
from cubicweb.view import EntityView, StartupView
from cubicweb.utils import json_dumps
--- a/web/views/timetable.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/timetable.py Tue Jul 10 15:07:52 2012 +0200
@@ -23,7 +23,7 @@
from logilab.mtconverter import xml_escape
from logilab.common.date import ONEDAY, date_range, todatetime
-from cubicweb.selectors import adaptable
+from cubicweb.predicates import adaptable
from cubicweb.view import EntityView
--- a/web/views/treeview.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/treeview.py Tue Jul 10 15:07:52 2012 +0200
@@ -27,10 +27,11 @@
from logilab.mtconverter import xml_escape
from cubicweb.utils import make_uid, json
-from cubicweb.selectors import adaptable
+from cubicweb.predicates import adaptable
from cubicweb.view import EntityView
from cubicweb.mixins import _done_init
from cubicweb.web.views import baseviews
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
def treecookiename(treeid):
return str('%s-treestate' % treeid)
@@ -280,3 +281,20 @@
treeid=treeid, initial_load=False, **morekwargs)
w(u'</li>')
+
+
+@ajaxfunc
+def node_clicked(self, treeid, nodeeid):
+ """add/remove eid in treestate cookie"""
+ cookies = self._cw.get_cookie()
+ statename = treecookiename(treeid)
+ treestate = cookies.get(statename)
+ if treestate is None:
+ self._cw.set_cookie(statename, nodeeid)
+ else:
+ marked = set(filter(None, treestate.value.split(':')))
+ if nodeeid in marked:
+ marked.remove(nodeeid)
+ else:
+ marked.add(nodeeid)
+ self._cw.set_cookie(statename, ':'.join(marked))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/undohistory.py Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/urlpublishing.py Tue Jul 10 15:07:52 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/vcard.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/vcard.py Tue Jul 10 15:07:52 2012 +0200
@@ -20,7 +20,7 @@
"""
__docformat__ = "restructuredtext en"
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.view import EntityView
_ = unicode
--- a/web/views/wdoc.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/wdoc.py Tue Jul 10 15:07:52 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.
@@ -27,9 +27,10 @@
from logilab.common.changelog import ChangeLog
from logilab.common.date import strptime, todate
+from logilab.common.registry import yes
from logilab.mtconverter import CHARSET_DECL_RGX
-from cubicweb.selectors import match_form_params, yes
+from cubicweb.predicates import match_form_params
from cubicweb.view import StartupView
from cubicweb.uilib import rest_publish
from cubicweb.web import NotFound, action
--- a/web/views/workflow.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/workflow.py Tue Jul 10 15:07:52 2012 +0200
@@ -31,7 +31,7 @@
from logilab.common.graph import escape
from cubicweb import Unauthorized
-from cubicweb.selectors import (has_related_entities, one_line_rset,
+from cubicweb.predicates import (has_related_entities, one_line_rset,
relation_possible, match_form_params,
score_entity, is_instance, adaptable)
from cubicweb.view import EntityView
--- a/web/views/xbel.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/xbel.py Tue Jul 10 15:07:52 2012 +0200
@@ -22,7 +22,7 @@
from logilab.mtconverter import xml_escape
-from cubicweb.selectors import is_instance
+from cubicweb.predicates import is_instance
from cubicweb.view import EntityView
from cubicweb.web.views.xmlrss import XMLView
--- a/web/views/xmlrss.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/views/xmlrss.py Tue Jul 10 15:07:52 2012 +0200
@@ -25,7 +25,7 @@
from logilab.mtconverter import xml_escape
-from cubicweb.selectors import (is_instance, non_final_entity, one_line_rset,
+from cubicweb.predicates import (is_instance, non_final_entity, one_line_rset,
appobject_selectable, adaptable)
from cubicweb.view import EntityView, EntityAdapter, AnyRsetView, Component
from cubicweb.view import implements_adapter_compat
--- a/web/webconfig.py Tue Jul 10 10:33:19 2012 +0200
+++ b/web/webconfig.py Tue Jul 10 15:07:52 2012 +0200
@@ -321,17 +321,20 @@
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.debugmode or self.mode == 'test'):
+ return 'data/'
+ else:
+ return 'data/%s/' % self.instance_md5_version()
def _build_ui_properties(self):
# self.datadir_url[:-1] to remove trailing /
--- a/wsgi/handler.py Tue Jul 10 10:33:19 2012 +0200
+++ b/wsgi/handler.py Tue Jul 10 15:07:52 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 Tue Jul 10 10:33:19 2012 +0200
+++ b/wsgi/request.py Tue Jul 10 15:07:52 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 Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,61 @@
+# 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, config, vreg=None):
+ self.config = config
+ self.vreg = vreg
+ self.socket = ctx.socket(zmq.REQ)
+ self.host = config.get('base-url')
+ self.socket.connect(self.host)
+
+ 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)