# HG changeset patch # User Sylvain Thénault # Date 1341925672 -7200 # Node ID a964c40adbe3760b511eb4c974eb4392945f055c # Parent 8af7c6d86efb2556a56b5fc47990f31b61677141# Parent 1931953226f29e4b24d60ca5acee4b418cc95753 stable is now 3.15 diff -r 8af7c6d86efb -r a964c40adbe3 .hgtags --- 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 diff -r 8af7c6d86efb -r a964c40adbe3 __init__.py --- 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 = {} diff -r 8af7c6d86efb -r a964c40adbe3 __pkginfo__.py --- 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', } diff -r 8af7c6d86efb -r a964c40adbe3 _exceptions.py --- 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 . -"""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 + diff -r 8af7c6d86efb -r a964c40adbe3 appobject.py --- 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 - - 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'' % (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) diff -r 8af7c6d86efb -r a964c40adbe3 cwconfig.py --- 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 diff -r 8af7c6d86efb -r a964c40adbe3 cwctl.py --- 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': '', - 'help': 'Pyro name server host. If not set, will be detected by ' - 'using a broadcast query.', + ('repo-uri', + {'short': 'H', 'type' : 'string', 'metavar': '://<[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() diff -r 8af7c6d86efb -r a964c40adbe3 cwvreg.py --- 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 . -""".. 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) diff -r 8af7c6d86efb -r a964c40adbe3 dbapi.py --- 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): diff -r 8af7c6d86efb -r a964c40adbe3 debian/changelog --- 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 Mon, 11 Jun 2012 09:45:24 +0200 + +cubicweb (3.15.0-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault Thu, 12 Apr 2012 13:52:05 +0200 + cubicweb (3.14.8-1) unstable; urgency=low * new upstream release diff -r 8af7c6d86efb -r a964c40adbe3 debian/control --- 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 diff -r 8af7c6d86efb -r a964c40adbe3 devtools/__init__.py --- 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() diff -r 8af7c6d86efb -r a964c40adbe3 devtools/devctl.py --- 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 diff -r 8af7c6d86efb -r a964c40adbe3 devtools/fake.py --- 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 = [] diff -r 8af7c6d86efb -r a964c40adbe3 devtools/test/data/views.py --- 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""" diff -r 8af7c6d86efb -r a964c40adbe3 devtools/testlib.py --- 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) diff -r 8af7c6d86efb -r a964c40adbe3 doc/3.15.rst --- /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 /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. diff -r 8af7c6d86efb -r a964c40adbe3 doc/book/en/admin/instance-config.rst --- 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. "`
.