# HG changeset patch # User Julien Cristau # Date 1386601990 -3600 # Node ID b1e933b0e8509c29055fa7c9ff0d8fd56faac7d9 # Parent 2dae5bf5ea68e8d543762bdf718f249db613fb3c# Parent cf27006ce8132b7db8510b9d909c281b64001af6 merge 3.17.11 diff -r cf27006ce813 -r b1e933b0e850 __init__.py --- a/__init__.py Fri Dec 06 17:20:59 2013 +0100 +++ b/__init__.py Mon Dec 09 16:13:10 2013 +0100 @@ -22,6 +22,8 @@ # ignore the pygments UserWarnings import warnings +import cPickle +import zlib warnings.filterwarnings('ignore', category=UserWarning, message='.*was already imported', module='.*pygments') @@ -120,6 +122,26 @@ binary.seek(0) return binary + def __eq__(self, other): + if not isinstance(other, Binary): + return False + return self.getvalue(), other.getvalue() + + + # Binary helpers to store/fetch python objects + + @classmethod + def zpickle(cls, obj): + """ return a Binary containing a gzipped pickle of obj """ + retval = cls() + retval.write(zlib.compress(cPickle.dumps(obj, protocol=2))) + return retval + + def unzpickle(self): + """ decompress and loads the stream before returning it """ + return cPickle.loads(zlib.decompress(self.getvalue())) + + def str_or_binary(value): if isinstance(value, Binary): return value @@ -127,7 +149,6 @@ BASE_CONVERTERS['Password'] = str_or_binary - # use this dictionary to rename entity types while keeping bw compat ETYPE_NAME_MAP = {} diff -r cf27006ce813 -r b1e933b0e850 __pkginfo__.py --- a/__pkginfo__.py Fri Dec 06 17:20:59 2013 +0100 +++ b/__pkginfo__.py Mon Dec 09 16:13:10 2013 +0100 @@ -40,10 +40,10 @@ ] __depends__ = { - 'logilab-common': '>= 0.59.0', + 'logilab-common': '>= 0.60.0', 'logilab-mtconverter': '>= 0.8.0', 'rql': '>= 0.31.2', - 'yams': '>= 0.37.0', + 'yams': '>= 0.39.0', #gettext # for xgettext, msgcat, etc... # web dependancies 'simplejson': '>= 2.0.9', diff -r cf27006ce813 -r b1e933b0e850 cubicweb.spec --- a/cubicweb.spec Fri Dec 06 17:20:59 2013 +0100 +++ b/cubicweb.spec Mon Dec 09 16:13:10 2013 +0100 @@ -23,7 +23,7 @@ Requires: %{python}-logilab-common >= 0.59.0 Requires: %{python}-logilab-mtconverter >= 0.8.0 Requires: %{python}-rql >= 0.31.2 -Requires: %{python}-yams >= 0.37.0 +Requires: %{python}-yams >= 0.39.0 Requires: %{python}-logilab-database >= 1.10.0 Requires: %{python}-passlib Requires: %{python}-lxml diff -r cf27006ce813 -r b1e933b0e850 cwconfig.py --- a/cwconfig.py Fri Dec 06 17:20:59 2013 +0100 +++ b/cwconfig.py Mon Dec 09 16:13:10 2013 +0100 @@ -53,8 +53,7 @@ If you are not administrator of you machine or if you need to play with some specific version of |cubicweb| you can use `virtualenv`_ a tool to create -isolated Python environments. Since version 3.9 |cubicweb| is **`virtualenv` -friendly** and won't write any file outside the virtualenv directory. +isolated Python environments. - instances are stored in :file:`/etc/cubicweb.d` - temporary files (such as pid file) in :file:`/var/run/cubicweb` @@ -206,7 +205,7 @@ """return a list of installed configurations in a directory according to \*-ctl files """ - return [name for name in ('repository', 'twisted', 'all-in-one') + return [name for name in ('repository', 'all-in-one') if exists(join(directory, '%s.conf' % name))] def guess_configuration(directory): @@ -328,7 +327,7 @@ # the format below can be useful to debug multi thread issues: # log_format = '%(asctime)s - [%(threadName)s] (%(name)s) %(levelname)s: %(message)s' # nor remove appobjects based on unused interface [???] - cleanup_interface_sobjects = True + cleanup_unused_appobjects = True if (CWDEV and _forced_mode != 'system'): mode = 'user' @@ -499,21 +498,11 @@ try: gendeps = getattr(pkginfo, key.replace('_cubes', '')) except AttributeError: - # bw compat - if hasattr(pkginfo, oldkey): - warn('[3.8] cube %s: %s is deprecated, use %s dict' - % (cube, oldkey, key), DeprecationWarning) - deps = getattr(pkginfo, oldkey) - else: - deps = {} + deps = {} else: deps = dict( (x[len('cubicweb-'):], v) for x, v in gendeps.iteritems() if x.startswith('cubicweb-')) - if not isinstance(deps, dict): - deps = dict((key, None) for key in deps) - warn('[3.8] cube %s should define %s as a dict' % (cube, key), - DeprecationWarning) for depcube in deps: try: newname = CW_MIGRATION_MAP[depcube] @@ -940,10 +929,9 @@ ' "cubicweb-ctl list")' % appid) return home - MODES = ('common', 'repository', 'Any', 'web') + MODES = ('common', 'repository', 'Any') MCOMPAT = {'all-in-one': MODES, - 'repository': ('common', 'repository', 'Any'), - 'twisted' : ('common', 'web'),} + 'repository': ('common', 'repository', 'Any')} @classmethod def accept_mode(cls, mode): #assert mode in cls.MODES, mode diff -r cf27006ce813 -r b1e933b0e850 cwctl.py --- a/cwctl.py Fri Dec 06 17:20:59 2013 +0100 +++ b/cwctl.py Mon Dec 09 16:13:10 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -266,12 +266,7 @@ if tinfo: descr = getattr(tinfo, 'description', '') if not descr: - descr = getattr(tinfo, 'short_desc', '') - if descr: - warn('[3.8] short_desc is deprecated, update %s' - ' pkginfo' % cube, DeprecationWarning) - else: - descr = tinfo.__doc__ + descr = tinfo.__doc__ if descr: print ' '+ ' \n'.join(descr.splitlines()) modes = detect_available_modes(cwcfg.cube_dir(cube)) @@ -357,7 +352,7 @@ }), ('config', {'short': 'c', 'type' : 'choice', 'metavar': '', - 'choices': ('all-in-one', 'repository', 'twisted'), + 'choices': ('all-in-one', 'repository'), 'default': 'all-in-one', 'help': 'installation type, telling which part of an instance ' 'should be installed. You can list available configurations using the' @@ -1039,9 +1034,56 @@ raise ConfigurationError('unknown configuration key "%s" for mode %s' % (key, appcfg.name)) appcfg.save() + +# WSGI ######### + +def wsgichoices(): + try: + from werkzeug import serving + except ImportError: + return ('stdlib',) + return ('stdlib', 'werkzeug') + +class WSGIDebugStartHandler(InstanceCommand): + """Start an interactive wsgi server """ + name = 'wsgi' + actionverb = 'started' + arguments = '' + options = ( + ('method', + {'short': 'm', + 'type': 'choice', + 'metavar': '', + 'default': 'stdlib', + 'choices': wsgichoices(), + 'help': 'wsgi utility/method'}), + ('loglevel', + {'short': 'l', + 'type' : 'choice', + 'metavar': '', + 'default': 'debug', + 'choices': ('debug', 'info', 'warning', 'error'), + 'help': 'debug if -D is set, error otherwise', + }), + ) + + def wsgi_instance(self, appid): + config = cwcfg.config_for(appid, debugmode=1) + init_cmdline_log_threshold(config, self['loglevel']) + assert config.name == 'all-in-one' + meth = self['method'] + if meth == 'stdlib': + from cubicweb.wsgi import server + else: + from cubicweb.wsgi import wz as server + return server.run(config) + + + for cmdcls in (ListCommand, CreateInstanceCommand, DeleteInstanceCommand, StartInstanceCommand, StopInstanceCommand, RestartInstanceCommand, + WSGIDebugStartHandler, ReloadConfigurationCommand, StatusCommand, UpgradeInstanceCommand, ListVersionsInstanceCommand, @@ -1052,6 +1094,8 @@ ): CWCTL.register(cmdcls) + + def run(args): """command line tool""" import os diff -r cf27006ce813 -r b1e933b0e850 cwvreg.py --- a/cwvreg.py Fri Dec 06 17:20:59 2013 +0100 +++ b/cwvreg.py Mon Dec 09 16:13:10 2013 +0100 @@ -211,8 +211,7 @@ from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER, onevent, Binary, UnknownProperty, UnknownEid) -from cubicweb.predicates import (implements, appobject_selectable, - _reset_is_instance_cache) +from cubicweb.predicates import appobject_selectable, _reset_is_instance_cache @onevent('before-registry-reload') @@ -230,15 +229,6 @@ sys.modules.pop('cubicweb.web.uicfg', None) sys.modules.pop('cubicweb.web.uihelper', None) -def use_interfaces(obj): - """return interfaces required by the given object by searching for - `implements` predicate - """ - impl = obj.__select__.search_selector(implements) - if impl: - return sorted(impl.expected_ifaces) - return () - def require_appobject(obj): """return appobjects required by the given object by searching for `appobject_selectable` predicate @@ -568,7 +558,6 @@ def reset(self): CW_EVENT_MANAGER.emit('before-registry-reset', self) super(CWRegistryStore, self).reset() - self._needs_iface = {} self._needs_appobject = {} # two special registries, propertydefs which care all the property # definitions, and propertyvals which contains values for those @@ -641,20 +630,6 @@ for obj in objects: obj.schema = schema - @deprecated('[3.9] use .register instead') - def register_if_interface_found(self, obj, ifaces, **kwargs): - """register `obj` but remove it if no entity class implements one of - the given `ifaces` interfaces at the end of the registration process. - - Extra keyword arguments are given to the - :meth:`~cubicweb.cwvreg.CWRegistryStore.register` function. - """ - self.register(obj, **kwargs) - if not isinstance(ifaces, (tuple, list)): - self._needs_iface[obj] = (ifaces,) - else: - self._needs_iface[obj] = ifaces - def register(self, obj, *args, **kwargs): """register `obj` application object into `registryname` or `obj.__registry__` if not specified, with identifier `oid` or @@ -665,15 +640,6 @@ """ obj = related_appobject(obj) super(CWRegistryStore, self).register(obj, *args, **kwargs) - # XXX bw compat - ifaces = use_interfaces(obj) - if ifaces: - if not obj.__name__.endswith('Adapter') and \ - any(iface for iface in ifaces if not isinstance(iface, basestring)): - warn('[3.9] %s: interfaces in implements selector are ' - 'deprecated in favor of adapters / adaptable ' - 'selector' % obj.__name__, DeprecationWarning) - self._needs_iface[obj] = ifaces depends_on = require_appobject(obj) if depends_on is not None: self._needs_appobject[obj] = depends_on @@ -687,41 +653,14 @@ def initialization_completed(self): """cw specific code once vreg initialization is completed: - * remove objects requiring a missing interface, unless - config.cleanup_interface_sobjects is false + * remove objects requiring a missing appobject, unless + config.cleanup_unused_appobjects is false * init rtags """ # we may want to keep interface dependent objects (e.g.for i18n # catalog generation) - if self.config.cleanup_interface_sobjects: - # XXX deprecated with cw 3.9: remove appobjects that don't support - # any available interface - implemented_interfaces = set() - if 'Any' in self.get('etypes', ()): - for etype in self.schema.entities(): - if etype.final: - continue - cls = self['etypes'].etype_class(etype) - if cls.__implements__: - warn('[3.9] %s: using __implements__/interfaces are ' - 'deprecated in favor of adapters' % cls.__name__, - DeprecationWarning) - for iface in cls.__implements__: - implemented_interfaces.update(iface.__mro__) - implemented_interfaces.update(cls.__mro__) - for obj, ifaces in self._needs_iface.items(): - ifaces = frozenset(isinstance(iface, basestring) - and iface in self.schema - and self['etypes'].etype_class(iface) - or iface - for iface in ifaces) - if not ('Any' in ifaces or ifaces & implemented_interfaces): - reg = self[obj_registries(obj)[0]] - self.debug('unregister %s (no implemented ' - 'interface among %s)', reg.objid(obj), ifaces) - self.unregister(obj) - # since 3.9: remove appobjects which depending on other, unexistant - # appobjects + if self.config.cleanup_unused_appobjects: + # remove appobjects which depend on other, unexistant appobjects for obj, (regname, regids) in self._needs_appobject.items(): try: registry = self[regname] @@ -740,8 +679,8 @@ if 'uicfg' in self: # 'uicfg' is not loaded in a pure repository mode for rtags in self['uicfg'].itervalues(): 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) + # don't check rtags if we don't want to cleanup_unused_appobjects + rtag.init(self.schema, check=self.config.cleanup_unused_appobjects) # rql parsing utilities #################################################### diff -r cf27006ce813 -r b1e933b0e850 dbapi.py --- a/dbapi.py Fri Dec 06 17:20:59 2013 +0100 +++ b/dbapi.py Mon Dec 09 16:13:10 2013 +0100 @@ -422,25 +422,6 @@ req.set_session(self.session, user) return req - @deprecated('[3.8] use direct access to req.session.data dictionary') - def session_data(self): - """return a dictionary containing session data""" - return self.session.data - - @deprecated('[3.8] use direct access to req.session.data dictionary') - def get_session_data(self, key, default=None, pop=False): - if pop: - return self.session.data.pop(key, default) - return self.session.data.get(key, default) - - @deprecated('[3.8] use direct access to req.session.data dictionary') - def set_session_data(self, key, value): - self.session.data[key] = value - - @deprecated('[3.8] use direct access to req.session.data dictionary') - def del_session_data(self, key): - self.session.data.pop(key, None) - # 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 @@ -486,7 +467,7 @@ def _txid(self): return self.connection._txid(self) - def execute(self, rql, args=None, eid_key=None, build_descr=True): + def execute(self, rql, args=None, build_descr=True): """execute a rql query, return resulting rows and their description in a :class:`~cubicweb.rset.ResultSet` object @@ -517,10 +498,6 @@ execute('Any X WHERE X eid %(x)s', {'x': 123}) """ - if eid_key is not None: - warn('[3.8] eid_key is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) - # XXX use named argument for build_descr in case repo is < 3.8 rset = self._repo.execute(self._sessid, rql, args, build_descr=build_descr, **self._txid()) rset.req = self.req diff -r cf27006ce813 -r b1e933b0e850 debian/control --- a/debian/control Fri Dec 06 17:20:59 2013 +0100 +++ b/debian/control Mon Dec 09 16:13:10 2013 +0100 @@ -15,7 +15,7 @@ python-unittest2 | python (>= 2.7), python-logilab-mtconverter, python-rql, - python-yams (>= 0.37), + python-yams (>= 0.39), python-lxml, Standards-Version: 3.9.1 Homepage: http://www.cubicweb.org @@ -131,6 +131,8 @@ python-fyzz, python-imaging, python-rdflib +Breaks: + cubicweb-inlinedit (<< 1.1.1), Description: web interface library for the CubicWeb framework CubicWeb is a semantic web application framework. . @@ -150,7 +152,7 @@ gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.59.0), - python-yams (>= 0.37.0), + python-yams (>= 0.39.0), python-rql (>= 0.31.2), python-lxml Recommends: @@ -158,6 +160,10 @@ python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core +Breaks: + cubicweb-comment (<< 1.9.1), + cubicweb-person (<< 1.8.0), + cubicweb-geocoding (<< 0.2.0), Description: common library for the CubicWeb framework CubicWeb is a semantic web application framework. . diff -r cf27006ce813 -r b1e933b0e850 devtools/__init__.py --- a/devtools/__init__.py Fri Dec 06 17:20:59 2013 +0100 +++ b/devtools/__init__.py Mon Dec 09 16:13:10 2013 +0100 @@ -38,7 +38,7 @@ from cubicweb import ExecutionError, BadConnectionId from cubicweb import schema, cwconfig from cubicweb.server.serverconfig import ServerConfiguration -from cubicweb.etwist.twconfig import TwistedConfiguration +from cubicweb.etwist.twconfig import WebConfigurationBase cwconfig.CubicWebConfiguration.cls_adjust_sys_path() @@ -214,12 +214,12 @@ return BASE_URL -class BaseApptestConfiguration(TestServerConfiguration, TwistedConfiguration): +class BaseApptestConfiguration(TestServerConfiguration, WebConfigurationBase): name = 'all-in-one' # so it search for all-in-one.conf, not repository.conf options = cwconfig.merge_options(TestServerConfiguration.options - + TwistedConfiguration.options) - cubicweb_appobject_path = TestServerConfiguration.cubicweb_appobject_path | TwistedConfiguration.cubicweb_appobject_path - cube_appobject_path = TestServerConfiguration.cube_appobject_path | TwistedConfiguration.cube_appobject_path + + WebConfigurationBase.options) + cubicweb_appobject_path = TestServerConfiguration.cubicweb_appobject_path | WebConfigurationBase.cubicweb_appobject_path + cube_appobject_path = TestServerConfiguration.cube_appobject_path | WebConfigurationBase.cube_appobject_path def available_languages(self, *args): return self.cw_languages() diff -r cf27006ce813 -r b1e933b0e850 devtools/devctl.py --- a/devtools/devctl.py Fri Dec 06 17:20:59 2013 +0100 +++ b/devtools/devctl.py Mon Dec 09 16:13:10 2013 +0100 @@ -46,7 +46,7 @@ a cube or for cubicweb (without a home) """ creating = True - cleanup_interface_sobjects = False + cleanup_unused_appobjects = False cubicweb_appobject_path = (ServerConfiguration.cubicweb_appobject_path | WebConfiguration.cubicweb_appobject_path) diff -r cf27006ce813 -r b1e933b0e850 devtools/testlib.py --- a/devtools/testlib.py Fri Dec 06 17:20:59 2013 +0100 +++ b/devtools/testlib.py Mon Dec 09 16:13:10 2013 +0100 @@ -419,13 +419,10 @@ return self.cnx.cursor(req or self.request()) @nocoverage - def execute(self, rql, args=None, eidkey=None, req=None): + def execute(self, rql, args=None, req=None): """executes , builds a resultset, and returns a couple (rset, req) where req is a FakeRequest """ - if eidkey is not None: - warn('[3.8] eidkey is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) req = req or self.request(rql=rql) return req.execute(unicode(rql), args) @@ -447,10 +444,7 @@ # server side db api ####################################################### - def sexecute(self, rql, args=None, eid_key=None): - if eid_key is not None: - warn('[3.8] eid_key is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) + def sexecute(self, rql, args=None): self.session.set_cnxset() return self.session.execute(rql, args) @@ -986,15 +980,6 @@ self.assertEqual(len(MAILBOX), nb_msgs) return messages - # deprecated ############################################################### - - @deprecated('[3.8] use self.execute(...).get_entity(0, 0)') - def entity(self, rql, args=None, eidkey=None, req=None): - if eidkey is not None: - warn('[3.8] eidkey is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) - return self.execute(rql, args, req=req).get_entity(0, 0) - # auto-populating test classes and utilities ################################### diff -r cf27006ce813 -r b1e933b0e850 doc/3.18.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/3.18.rst Mon Dec 09 16:13:10 2013 +0100 @@ -0,0 +1,41 @@ +What's new in CubicWeb 3.18? +============================ + +New functionalities +-------------------- + +* add a security debugging tool + (see `#2920304 `_) + + +API changes +----------- + +* not really an API change, but the entity permission checks are now + systematically deferred to an operation, instead of a) trying in a + hook and b) if it failed, retrying later in an operation + +* The default value storage for attributes is no longer String, but + Bytes. This opens the road to storing arbitrary python objects, e.g. + numpy arrays, and fixes a bug where default values whose truth value + was False were not properly migrated. + +Deprecation +--------------------- + +* the old multi-source system + + +Deprecated Code Drops +---------------------- + +* ``ldapuser`` have been dropped; use ``ldapfeed`` now + (see `#2936496 `_) + +* action ``GotRhythm`` was removed, make sure you do not + import it in your cubes (even to unregister it) + (see `#3093362 `_) + +* all 3.8 backward compat is gone + +* all 3.9 backward compat (including the javascript side) is gone diff -r cf27006ce813 -r b1e933b0e850 doc/4.0.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/4.0.rst Mon Dec 09 16:13:10 2013 +0100 @@ -0,0 +1,8 @@ +What's new in CubicWeb 4.0? +============================ + +Deprecated Code Drops +---------------------- + +* The ldapuser source has been dropped. ldapfeed is the only official source + remaining for ldap. diff -r cf27006ce813 -r b1e933b0e850 doc/book/en/admin/config.rst --- a/doc/book/en/admin/config.rst Fri Dec 06 17:20:59 2013 +0100 +++ b/doc/book/en/admin/config.rst Mon Dec 09 16:13:10 2013 +0100 @@ -57,19 +57,28 @@ PostgreSQL ~~~~~~~~~~ -For installation, please refer to the `PostgreSQL project online documentation`_. - -.. _`PostgreSQL project online documentation`: http://www.postgresql.org/ +Many Linux distributions ship with the appropriate PostgreSQL packages. +Basically, you need to install the following packages: -You need to install the three following packages: `postgresql-8.X`, -`postgresql-client-8.X`, and `postgresql-plpython-8.X`. If you run postgres -version prior to 8.3, you'll also need the `postgresql-contrib-8.X` package for -full-text search extension. +* `postgresql` and `postgresql-client`, which will pull the respective + versioned packages (e.g. `postgresql-9.1` and `postgresql-client-9.1`) and, + optionally, +* a `postgresql-plpython-X.Y` package with a version corresponding to that of + the aforementioned packages (e.g. `postgresql-plpython-9.1`). + +If you run postgres version prior to 8.3, you'll also need the +`postgresql-contrib-8.X` package for full-text search extension. If you run postgres on another host than the |cubicweb| repository, you should install the `postgresql-client` package on the |cubicweb| host, and others on the database host. +For extra details concerning installation, please refer to the `PostgreSQL +project online documentation`_. + +.. _`PostgreSQL project online documentation`: http://www.postgresql.org/docs + + Database cluster ++++++++++++++++ diff -r cf27006ce813 -r b1e933b0e850 doc/book/en/admin/setup.rst --- a/doc/book/en/admin/setup.rst Fri Dec 06 17:20:59 2013 +0100 +++ b/doc/book/en/admin/setup.rst Mon Dec 09 16:13:10 2013 +0100 @@ -121,10 +121,10 @@ `Virtualenv` install -------------------- -Since version 3.9, |cubicweb| can be safely installed, used and contained inside -a `virtualenv`_. You can use either :ref:`pip ` or -:ref:`easy_install ` to install |cubicweb| inside an -activated virtual environment. +|cubicweb| can be safely installed, used and contained inside a +`virtualenv`_. You can use either :ref:`pip ` or +:ref:`easy_install ` to install |cubicweb| +inside an activated virtual environment. .. _PipInstallation: diff -r cf27006ce813 -r b1e933b0e850 doc/book/en/devrepo/datamodel/definition.rst --- a/doc/book/en/devrepo/datamodel/definition.rst Fri Dec 06 17:20:59 2013 +0100 +++ b/doc/book/en/devrepo/datamodel/definition.rst Mon Dec 09 16:13:10 2013 +0100 @@ -226,13 +226,13 @@ * `SizeConstraint`: allows to specify a minimum and/or maximum size on string (generic case of `maxsize`) -* `BoundConstraint`: allows to specify a minimum and/or maximum value +* `BoundaryConstraint`: allows to specify a minimum and/or maximum value on numeric types and date .. sourcecode:: python - from yams.constraints import BoundConstraint, TODAY - BoundConstraint('<=', TODAY()) + from yams.constraints import BoundaryConstraint, TODAY + BoundaryConstraint('<=', TODAY()) * `IntervalBoundConstraint`: allows to specify an interval with included values @@ -476,13 +476,8 @@ Here are the current rules: -1. permission to add/update entity and its attributes are checked: - - - on commit if the entity has been added - - - in an 'after_update_entity' hook if the entity has been updated. If it fails - at this time, it will be retried on commit (hence you get the permission if - you have it just after the modification or *at* commit time) +1. permission to add/update entity and its attributes are checked on + commit 2. permission to delete an entity is checked in 'before_delete_entity' hook diff -r cf27006ce813 -r b1e933b0e850 doc/book/en/devrepo/entityclasses/adapters.rst --- a/doc/book/en/devrepo/entityclasses/adapters.rst Fri Dec 06 17:20:59 2013 +0100 +++ b/doc/book/en/devrepo/entityclasses/adapters.rst Mon Dec 09 16:13:10 2013 +0100 @@ -10,13 +10,7 @@ .. _`interfaces`: http://java.sun.com/docs/books/tutorial/java/concepts/interface.html .. _`adapter`: http://en.wikipedia.org/wiki/Adapter_pattern -In |cubicweb| adapters provide logical functionalities to entity types. They -are introduced in version `3.9`. Before that one had to implement Interfaces in -entity classes to achieve a similar goal. However, the problem with this -approach is that is clutters the entity class's namespace, exposing name -collision risks with schema attributes/relations or even methods names -(different interfaces may define the same method with not necessarily the same -behaviour expected). +In |cubicweb| adapters provide logical functionalities to entity types. Definition of an adapter is quite trivial. An excerpt from cubicweb itself (found in :mod:`cubicweb.entities.adapters`): diff -r cf27006ce813 -r b1e933b0e850 doc/book/en/devrepo/migration.rst --- a/doc/book/en/devrepo/migration.rst Fri Dec 06 17:20:59 2013 +0100 +++ b/doc/book/en/devrepo/migration.rst Mon Dec 09 16:13:10 2013 +0100 @@ -46,7 +46,7 @@ Again in the directory `migration`, the file `depends.map` allows to indicate that for the migration to a particular model version, you always have to first migrate to a particular *CubicWeb* version. This file can contain comments (lines -starting by `#`) and a dependancy is listed as follows: :: +starting with `#`) and a dependency is listed as follows: :: : @@ -170,9 +170,9 @@ * `rql(rql, kwargs=None, cachekey=None, ask_confirm=True)`, executes an arbitrary RQL query, either to interrogate or update. A result set object is returned. -* `add_entity(etype, *args, **kwargs)`, adds a nes entity type of the given - type. The attribute and relation values are specified using the named and - positionned parameters. +* `add_entity(etype, *args, **kwargs)`, adds a new entity of the given type. + The attribute and relation values are specified as named positional + arguments. Workflow creation ----------------- diff -r cf27006ce813 -r b1e933b0e850 doc/book/en/devrepo/testing.rst --- a/doc/book/en/devrepo/testing.rst Fri Dec 06 17:20:59 2013 +0100 +++ b/doc/book/en/devrepo/testing.rst Mon Dec 09 16:13:10 2013 +0100 @@ -18,12 +18,7 @@ convenience methods to help test all of this. In the realm of views, automatic tests check that views are valid -XHTML. See :ref:`automatic_views_tests` for details. Since 3.9, bases -for web functional testing using `windmill -`_ are set. See test cases in -cubicweb/web/test/windmill and python wrapper in -cubicweb/web/test_windmill/ if you want to use this in your own cube. - +XHTML. See :ref:`automatic_views_tests` for details. Most unit tests need a live database to work against. This is achieved by CubicWeb using automatically sqlite (bundled with Python, see diff -r cf27006ce813 -r b1e933b0e850 doc/book/en/tutorials/advanced/part04_ui-base.rst --- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Fri Dec 06 17:20:59 2013 +0100 +++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst Mon Dec 09 16:13:10 2013 +0100 @@ -194,8 +194,6 @@ .. Note:: - * Adapters have been introduced in CubicWeb 3.9 / cubicweb-folder 1.8. - * As seen earlier, we want to **replace** the folder's `ITree` adapter by our implementation, hence the custom `registration_callback` method. @@ -241,12 +239,6 @@ ascendant/descendant ordering and a strict comparison with current file's name (the "X" variable representing the current file). -.. Note:: - - * Former `implements` selector should be replaced by one of `is_instance` / - `adaptable` selector with CubicWeb >= 3.9. In our case, `is_instance` to - tell our adapter is able to adapt `File` entities. - Notice that this query supposes we wont have two files of the same name in the same folder, else things may go wrong. Fixing this is out of the scope of this blog. And as I would like to have at some point a smarter, context sensitive @@ -358,7 +350,7 @@ You'll have to answer some questions, as we've seen in `an earlier post`_. Now that everything is tested, I can transfer the new code to the production -server, `apt-get upgrade` cubicweb 3.9 and its dependencies, and eventually +server, `apt-get upgrade` cubicweb and its dependencies, and eventually upgrade the production instance. diff -r cf27006ce813 -r b1e933b0e850 doc/book/en/tutorials/advanced/part05_ui-advanced.rst --- a/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Fri Dec 06 17:20:59 2013 +0100 +++ b/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Mon Dec 09 16:13:10 2013 +0100 @@ -1,8 +1,6 @@ Building my photos web site with |cubicweb| part V: let's make it even more user friendly ========================================================================================= -We'll now see how to benefit from features introduced in 3.9 and 3.10 releases of CubicWeb - .. _uiprops: Step 1: tired of the default look? @@ -29,9 +27,9 @@ LOGO = data('logo.jpg') -The uiprops machinery has been introduced in `CubicWeb 3.9`_. It is used to define -some static file resources, such as the logo, default Javascript / CSS files, as -well as CSS properties (we'll see that later). +The uiprops machinery is used to define some static file resources, +such as the logo, default Javascript / CSS files, as well as CSS +properties (we'll see that later). .. Note:: This file is imported specifically by |cubicweb|, with a predefined name space, @@ -373,5 +371,4 @@ .. _`CubicWeb 3.10`: http://www.cubicweb.org/blogentry/1330518 -.. _`CubicWeb 3.9`: http://www.cubicweb.org/blogentry/1179899 .. _`here`: http://webdesign.about.com/od/css3/f/blfaqbgsize.htm diff -r cf27006ce813 -r b1e933b0e850 doc/tools/pyjsrest.py --- a/doc/tools/pyjsrest.py Fri Dec 06 17:20:59 2013 +0100 +++ b/doc/tools/pyjsrest.py Mon Dec 09 16:13:10 2013 +0100 @@ -136,7 +136,6 @@ 'cubicweb.preferences', 'cubicweb.edition', 'cubicweb.reledit', - 'cubicweb.rhythm', 'cubicweb.timeline-ext', ] diff -r cf27006ce813 -r b1e933b0e850 entities/adapters.py --- a/entities/adapters.py Fri Dec 06 17:20:59 2013 +0100 +++ b/entities/adapters.py Mon Dec 09 16:13:10 2013 +0100 @@ -28,9 +28,7 @@ from logilab.common.decorators import cached from cubicweb import ValidationError, view -from cubicweb.predicates import (implements, is_instance, relation_possible, - match_exception) -from cubicweb.interfaces import IDownloadable, ITree +from cubicweb.predicates import is_instance, relation_possible, match_exception class IEmailableAdapter(view.EntityAdapter): @@ -67,11 +65,9 @@ class INotifiableAdapter(view.EntityAdapter): - __needs_bw_compat__ = True __regid__ = 'INotifiable' __select__ = is_instance('Any') - @view.implements_adapter_compat('INotifiableAdapter') def notification_references(self, view): """used to control References field of email send on notification for this entity. `view` is the notification view. @@ -167,27 +163,25 @@ class IDownloadableAdapter(view.EntityAdapter): """interface for downloadable entities""" - __needs_bw_compat__ = True __regid__ = 'IDownloadable' - __select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract + __abstract__ = True - @view.implements_adapter_compat('IDownloadable') def download_url(self, **kwargs): # XXX not really part of this interface """return an url to download entity's content""" raise NotImplementedError - @view.implements_adapter_compat('IDownloadable') + def download_content_type(self): """return MIME type of the downloadable content""" raise NotImplementedError - @view.implements_adapter_compat('IDownloadable') + def download_encoding(self): """return encoding of the downloadable content""" raise NotImplementedError - @view.implements_adapter_compat('IDownloadable') + def download_file_name(self): """return file name of the downloadable content""" raise NotImplementedError - @view.implements_adapter_compat('IDownloadable') + def download_data(self): """return actual data of the downloadable content""" raise NotImplementedError @@ -219,27 +213,16 @@ .. automethod: children_rql .. automethod: path """ - __needs_bw_compat__ = True __regid__ = 'ITree' - __select__ = implements(ITree, warn=False) # XXX for bw compat, else should be abstract + __abstract__ = True child_role = 'subject' parent_role = 'object' - @property - def tree_relation(self): - warn('[3.9] tree_attribute is deprecated, define tree_relation on a custom ' - 'ITree for %s instead' % (self.entity.__class__), - DeprecationWarning) - return self.entity.tree_attribute - - # XXX should be removed from the public interface - @view.implements_adapter_compat('ITree') def children_rql(self): """Returns RQL to get the children of the entity.""" return self.entity.cw_related_rql(self.tree_relation, self.parent_role) - @view.implements_adapter_compat('ITree') def different_type_children(self, entities=True): """Return children entities of different type as this entity. @@ -253,7 +236,6 @@ return [e for e in res if e.e_schema != eschema] return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col) - @view.implements_adapter_compat('ITree') def same_type_children(self, entities=True): """Return children entities of the same type as this entity. @@ -267,23 +249,19 @@ return [e for e in res if e.e_schema == eschema] return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col) - @view.implements_adapter_compat('ITree') def is_leaf(self): """Returns True if the entity does not have any children.""" return len(self.children()) == 0 - @view.implements_adapter_compat('ITree') def is_root(self): """Returns true if the entity is root of the tree (e.g. has no parent). """ return self.parent() is None - @view.implements_adapter_compat('ITree') def root(self): """Return the root entity of the tree.""" return self._cw.entity_from_eid(self.path()[0]) - @view.implements_adapter_compat('ITree') def parent(self): """Returns the parent entity if any, else None (e.g. if we are on the root). @@ -294,7 +272,6 @@ except (KeyError, IndexError): return None - @view.implements_adapter_compat('ITree') def children(self, entities=True, sametype=False): """Return children entities. @@ -307,7 +284,6 @@ return self.entity.related(self.tree_relation, self.parent_role, entities=entities) - @view.implements_adapter_compat('ITree') def iterparents(self, strict=True): """Return an iterator on the parents of the entity.""" def _uptoroot(self): @@ -322,7 +298,6 @@ return chain([self.entity], _uptoroot(self)) return _uptoroot(self) - @view.implements_adapter_compat('ITree') def iterchildren(self, _done=None): """Return an iterator over the item's children.""" if _done is None: @@ -334,7 +309,6 @@ yield child _done.add(child.eid) - @view.implements_adapter_compat('ITree') def prefixiter(self, _done=None): """Return an iterator over the item's descendants in a prefixed order.""" if _done is None: @@ -347,7 +321,6 @@ for entity in child.cw_adapt_to('ITree').prefixiter(_done): yield entity - @view.implements_adapter_compat('ITree') @cached def path(self): """Returns the list of eids from the root object to this object.""" diff -r cf27006ce813 -r b1e933b0e850 entities/test/unittest_base.py --- a/entities/test/unittest_base.py Fri Dec 06 17:20:59 2013 +0100 +++ b/entities/test/unittest_base.py Mon Dec 09 16:13:10 2013 +0100 @@ -25,7 +25,6 @@ from cubicweb.devtools.testlib import CubicWebTC -from cubicweb.interfaces import IMileStone, ICalendarable from cubicweb.entities import AnyEntity @@ -134,27 +133,6 @@ self.request().create_entity('CWGroup', name=u'logilab', reverse_in_group=e) -class InterfaceTC(CubicWebTC): - - def test_nonregr_subclasses_and_mixins_interfaces(self): - from cubicweb.entities.wfobjs import WorkflowableMixIn - WorkflowableMixIn.__implements__ = (ICalendarable,) - CWUser = self.vreg['etypes'].etype_class('CWUser') - class MyUser(CWUser): - __implements__ = (IMileStone,) - self.vreg._loadedmods[__name__] = {} - self.vreg.register(MyUser) - self.vreg['etypes'].initialization_completed() - MyUser_ = self.vreg['etypes'].etype_class('CWUser') - # a copy is done systematically - self.assertTrue(issubclass(MyUser_, MyUser)) - self.assertTrue(implements(MyUser_, IMileStone)) - self.assertTrue(implements(MyUser_, ICalendarable)) - # original class should not have beed modified, only the copy - self.assertTrue(implements(MyUser, IMileStone)) - self.assertFalse(implements(MyUser, ICalendarable)) - - class SpecializedEntityClassesTC(CubicWebTC): def select_eclass(self, etype): diff -r cf27006ce813 -r b1e933b0e850 entities/wfobjs.py --- a/entities/wfobjs.py Fri Dec 06 17:20:59 2013 +0100 +++ b/entities/wfobjs.py Mon Dec 09 16:13:10 2013 +0100 @@ -32,7 +32,6 @@ from cubicweb.entities import AnyEntity, fetch_config from cubicweb.view import EntityAdapter from cubicweb.predicates import relation_possible -from cubicweb.mixins import MI_REL_TRIGGERS class WorkflowException(Exception): pass @@ -379,65 +378,8 @@ return self.by_transition and self.by_transition[0] or None -class WorkflowableMixIn(object): - """base mixin providing workflow helper methods for workflowable entities. - This mixin will be automatically set on class supporting the 'in_state' - relation (which implies supporting 'wf_info_for' as well) - """ - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').main_workflow") - def main_workflow(self): - return self.cw_adapt_to('IWorkflowable').main_workflow - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_workflow") - def current_workflow(self): - return self.cw_adapt_to('IWorkflowable').current_workflow - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_state") - def current_state(self): - return self.cw_adapt_to('IWorkflowable').current_state - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').state") - def state(self): - return self.cw_adapt_to('IWorkflowable').state - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').printable_state") - def printable_state(self): - return self.cw_adapt_to('IWorkflowable').printable_state - @property - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').workflow_history") - def workflow_history(self): - return self.cw_adapt_to('IWorkflowable').workflow_history - - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').cwetype_workflow()") - def cwetype_workflow(self): - return self.cw_adapt_to('IWorkflowable').main_workflow() - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').latest_trinfo()") - def latest_trinfo(self): - return self.cw_adapt_to('IWorkflowable').latest_trinfo() - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').possible_transitions()") - def possible_transitions(self, type='normal'): - return self.cw_adapt_to('IWorkflowable').possible_transitions(type) - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').fire_transition()") - def fire_transition(self, tr, comment=None, commentformat=None): - return self.cw_adapt_to('IWorkflowable').fire_transition(tr, comment, commentformat) - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').change_state()") - def change_state(self, statename, comment=None, commentformat=None, tr=None): - return self.cw_adapt_to('IWorkflowable').change_state(statename, comment, commentformat, tr) - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()") - def subworkflow_input_trinfo(self): - return self.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo() - @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_transition()") - def subworkflow_input_transition(self): - return self.cw_adapt_to('IWorkflowable').subworkflow_input_transition() - - -MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn - - - -class IWorkflowableAdapter(WorkflowableMixIn, EntityAdapter): +class IWorkflowableAdapter(EntityAdapter): """base adapter providing workflow helper methods for workflowable entities. """ __regid__ = 'IWorkflowable' diff -r cf27006ce813 -r b1e933b0e850 entity.py --- a/entity.py Fri Dec 06 17:20:59 2013 +0100 +++ b/entity.py Mon Dec 09 16:13:10 2013 +0100 @@ -42,7 +42,6 @@ from cubicweb.rqlrewrite import RQLRewriter from cubicweb.uilib import soup2xhtml -from cubicweb.mixins import MI_REL_TRIGGERS from cubicweb.mttransforms import ENGINE _marker = object() @@ -194,31 +193,11 @@ setattr(cls, rschema.type, Attribute(rschema.type)) mixins = [] for rschema, _, role in eschema.relation_definitions(): - if (rschema, role) in MI_REL_TRIGGERS: - mixin = MI_REL_TRIGGERS[(rschema, role)] - if not (issubclass(cls, mixin) or mixin in mixins): # already mixed ? - mixins.append(mixin) - for iface in getattr(mixin, '__implements__', ()): - if not interface.implements(cls, iface): - interface.extend(cls, iface) if role == 'subject': attr = rschema.type else: attr = 'reverse_%s' % rschema.type setattr(cls, attr, Relation(rschema, role)) - if mixins: - # see etype class instantation in cwvreg.ETypeRegistry.etype_class method: - # due to class dumping, cls is the generated top level class with actual - # user class as (only) parent. Since we want to be able to override mixins - # method from this user class, we have to take care to insert mixins after that - # class - # - # note that we don't plug mixins as user class parent since it causes pb - # with some cases of entity classes inheritance. - mixins.insert(0, cls.__bases__[0]) - mixins += cls.__bases__[1:] - cls.__bases__ = tuple(mixins) - cls.info('plugged %s mixins on %s', mixins, cls) fetch_attrs = ('modification_date',) @@ -558,7 +537,14 @@ raise NotImplementedError('comparison not implemented for %s' % self.__class__) def __eq__(self, other): - raise NotImplementedError('comparison not implemented for %s' % self.__class__) + if isinstance(self.eid, (int, long)): + return self.eid == other.eid + return self is other + + def __hash__(self): + if isinstance(self.eid, (int, long)): + return self.eid + return super(Entity, self).__hash__() def _cw_update_attr_cache(self, attrcache): # if context is a repository session, don't consider dont-cache-attrs as @@ -983,7 +969,7 @@ return value def related(self, rtype, role='subject', limit=None, entities=False, # XXX .cw_related - safe=False): + safe=False, targettypes=None): """returns a resultset of related entities :param rtype: @@ -997,10 +983,13 @@ :param safe: if True, an empty rset/list of entities will be returned in case of :exc:`Unauthorized`, else (the default), the exception is propagated + :param targettypes: + a tuple of target entity types to restrict the query """ rtype = str(rtype) - if limit is None: - # we cannot do much wrt cache on limited queries + # Caching restricted/limited results is best avoided. + cacheable = limit is None and targettypes is None + if cacheable: cache_key = '%s_%s' % (rtype, role) if cache_key in self._cw_related_cache: return self._cw_related_cache[cache_key][entities] @@ -1008,7 +997,7 @@ if entities: return [] return self._cw.empty_rset() - rql = self.cw_related_rql(rtype, role, limit=limit) + rql = self.cw_related_rql(rtype, role, limit=limit, targettypes=targettypes) try: rset = self._cw.execute(rql, {'x': self.eid}) except Unauthorized: @@ -1016,9 +1005,9 @@ raise rset = self._cw.empty_rset() if entities: - if limit is None: + if cacheable: self.cw_set_relation_cache(rtype, role, rset) - return self.related(rtype, role, limit, entities) + return self.related(rtype, role, entities=entities) return list(rset.entities()) else: return rset @@ -1340,34 +1329,6 @@ def clear_all_caches(self): return self.cw_clear_all_caches() - @deprecated('[3.9] use entity.cw_attr_value(attr)') - def get_value(self, name): - return self.cw_attr_value(name) - - @deprecated('[3.9] use entity.cw_delete()') - def delete(self, **kwargs): - return self.cw_delete(**kwargs) - - @deprecated('[3.9] use entity.cw_attr_metadata(attr, metadata)') - def attr_metadata(self, attr, metadata): - return self.cw_attr_metadata(attr, metadata) - - @deprecated('[3.9] use entity.cw_has_perm(action)') - def has_perm(self, action): - return self.cw_has_perm(action) - - @deprecated('[3.9] use entity.cw_set_relation_cache(rtype, role, rset)') - def set_related_cache(self, rtype, role, rset): - self.cw_set_relation_cache(rtype, role, rset) - - @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role)') - def clear_related_cache(self, rtype=None, role=None): - self.cw_clear_relation_cache(rtype, role) - - @deprecated('[3.9] use entity.cw_related_rql(rtype, [role, [targettypes]])') - def related_rql(self, rtype, role='subject', targettypes=None): - return self.cw_related_rql(rtype, role, targettypes) - @property @deprecated('[3.10] use entity.cw_edited') def edited_attributes(self): diff -r cf27006ce813 -r b1e933b0e850 etwist/request.py --- a/etwist/request.py Fri Dec 06 17:20:59 2013 +0100 +++ b/etwist/request.py Mon Dec 09 16:13:10 2013 +0100 @@ -24,15 +24,21 @@ class CubicWebTwistedRequestAdapter(CubicWebRequestBase): + """ from twisted .req to cubicweb .form + req.files are put into .form[] + """ def __init__(self, req, vreg, https): self._twreq = req 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) + for key, name_stream_list in req.files.iteritems(): + for name, stream in name_stream_list: + if name is not None: + name = unicode(name, self.encoding) + self.form.setdefault(key, []).append((name, stream)) + # 3.16.4 backward compat + if len(self.form[key]) == 1: + self.form[key] = self.form[key][0] self.content = self._twreq.content # stream def http_method(self): diff -r cf27006ce813 -r b1e933b0e850 etwist/server.py --- a/etwist/server.py Fri Dec 06 17:20:59 2013 +0100 +++ b/etwist/server.py Mon Dec 09 16:13:10 2013 +0100 @@ -244,7 +244,6 @@ self._do_process_multipart = True self.process() - @monkeypatch(http.Request) def process_multipart(self): if not self._do_process_multipart: @@ -254,16 +253,17 @@ keep_blank_values=1, strict_parsing=1) for key in form: - value = form[key] - if isinstance(value, list): - self.args[key] = [v.value for v in value] - elif value.filename: - if value.done != -1: # -1 is transfer has been interrupted - self.files[key] = (value.filename, value.file) + values = form[key] + if not isinstance(values, list): + values = [values] + for value in values: + if value.filename: + if value.done != -1: # -1 is transfer has been interrupted + self.files.setdefault(key, []).append((value.filename, value.file)) + else: + self.files.setdefault(key, []).append((None, None)) else: - self.files[key] = (None, None) - else: - self.args[key] = value.value + self.args.setdefault(key, []).append(value.value) from logging import getLogger from cubicweb import set_log_methods diff -r cf27006ce813 -r b1e933b0e850 etwist/twconfig.py --- a/etwist/twconfig.py Fri Dec 06 17:20:59 2013 +0100 +++ b/etwist/twconfig.py Mon Dec 09 16:13:10 2013 +0100 @@ -34,9 +34,8 @@ from cubicweb.web.webconfig import WebConfiguration -class TwistedConfiguration(WebConfiguration): +class WebConfigurationBase(WebConfiguration): """web instance (in a twisted web server) client of a RQL server""" - name = 'twisted' options = merge_options(( # ctl configuration @@ -107,19 +106,17 @@ return 'http://%s:%s/' % (self['host'] or getfqdn(), self['port'] or 8080) -CONFIGURATIONS.append(TwistedConfiguration) - try: from cubicweb.server.serverconfig import ServerConfiguration - class AllInOneConfiguration(TwistedConfiguration, ServerConfiguration): + class AllInOneConfiguration(WebConfigurationBase, ServerConfiguration): """repository and web instance in the same twisted process""" name = 'all-in-one' - options = merge_options(TwistedConfiguration.options + options = merge_options(WebConfigurationBase.options + ServerConfiguration.options) - cubicweb_appobject_path = TwistedConfiguration.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path - cube_appobject_path = TwistedConfiguration.cube_appobject_path | ServerConfiguration.cube_appobject_path + cubicweb_appobject_path = WebConfigurationBase.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path + cube_appobject_path = WebConfigurationBase.cube_appobject_path | ServerConfiguration.cube_appobject_path def pyro_enabled(self): """tell if pyro is activated for the in memory repository""" return self['pyro-server'] diff -r cf27006ce813 -r b1e933b0e850 ext/rest.py --- a/ext/rest.py Fri Dec 06 17:20:59 2013 +0100 +++ b/ext/rest.py Mon Dec 09 16:13:10 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -29,6 +29,8 @@ * `sourcecode` (if pygments is installed), source code colorization +* `rql-table`, create a table from a RQL query + """ __docformat__ = "restructuredtext en" @@ -40,7 +42,7 @@ from docutils import statemachine, nodes, utils, io from docutils.core import Publisher -from docutils.parsers.rst import Parser, states, directives +from docutils.parsers.rst import Parser, states, directives, Directive from docutils.parsers.rst.roles import register_canonical_role, set_classes from logilab.mtconverter import ESC_UCAR_TABLE, ESC_CAR_TABLE, xml_escape @@ -251,6 +253,76 @@ winclude_directive.options = {'literal': directives.flag, 'encoding': directives.encoding} +class RQLTableDirective(Directive): + """rql-table directive + + Example: + + .. rql-table:: + :vid: mytable + :headers: , , progress + :colvids: 2=progress + + Any X,U,X WHERE X is Project, X url U + + All fields but the RQL string are optionnal. The ``:headers:`` option can + contain empty column names. + """ + + required_arguments = 0 + optional_arguments = 0 + has_content= True + final_argument_whitespace = True + option_spec = {'vid': directives.unchanged, + 'headers': directives.unchanged, + 'colvids': directives.unchanged} + + def run(self): + errid = "rql-table directive" + self.assert_has_content() + if self.arguments: + raise self.warning('%s does not accept arguments' % errid) + rql = ' '.join([l.strip() for l in self.content]) + _cw = self.state.document.settings.context._cw + _cw.ensure_ro_rql(rql) + try: + rset = _cw.execute(rql) + except Exception as exc: + raise self.error("fail to execute RQL query in %s: %r" % + (errid, exc)) + if not rset: + raise self.warning("empty result set") + vid = self.options.get('vid', 'table') + try: + view = _cw.vreg['views'].select(vid, _cw, rset=rset) + except Exception as exc: + raise self.error("fail to select '%s' view in %s: %r" % + (vid, errid, exc)) + headers = None + if 'headers' in self.options: + headers = [h.strip() for h in self.options['headers'].split(',')] + while headers.count(''): + headers[headers.index('')] = None + if len(headers) != len(rset[0]): + raise self.error("the number of 'headers' does not match the " + "number of columns in %s" % errid) + cellvids = None + if 'colvids' in self.options: + cellvids = {} + for f in self.options['colvids'].split(','): + try: + idx, vid = f.strip().split('=') + except ValueError: + raise self.error("malformatted 'colvids' option in %s" % + errid) + cellvids[int(idx.strip())] = vid.strip() + try: + content = view.render(headers=headers, cellvids=cellvids) + except Exception as exc: + raise self.error("Error rendering %s (%s)" % (errid, exc)) + return [nodes.raw('', content, format='html')] + + try: from pygments import highlight from pygments.lexers import get_lexer_by_name @@ -385,3 +457,4 @@ directives.register_directive('winclude', winclude_directive) if pygments_directive is not None: directives.register_directive('sourcecode', pygments_directive) + directives.register_directive('rql-table', RQLTableDirective) diff -r cf27006ce813 -r b1e933b0e850 ext/test/data/views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ext/test/data/views.py Mon Dec 09 16:13:10 2013 +0100 @@ -0,0 +1,24 @@ +# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . + + +from cubicweb.web.views import tableview + +class CustomRsetTableView(tableview.RsetTableView): + __regid__ = 'mytable' + diff -r cf27006ce813 -r b1e933b0e850 ext/test/unittest_rest.py --- a/ext/test/unittest_rest.py Fri Dec 06 17:20:59 2013 +0100 +++ b/ext/test/unittest_rest.py Mon Dec 09 16:13:10 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -82,5 +82,133 @@ out = rest_publish(context, ':bookmark:`%s`' % eid) self.assertEqual(out, u'

CWUser_plural

\n') + def test_rqltable_nocontent(self): + context = self.context() + out = rest_publish(context, """.. rql-table::""") + self.assertIn("System Message: ERROR", out) + self.assertIn("Content block expected for the "rql-table" " + "directive; none found" , out) + + def test_rqltable_norset(self): + context = self.context() + rql = "Any X WHERE X is CWUser, X firstname 'franky'" + out = rest_publish( + context, """\ +.. rql-table:: + + %(rql)s""" % {'rql': rql}) + self.assertIn("System Message: WARNING", out) + self.assertIn("empty result set", out) + + def test_rqltable_nooptions(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + out = rest_publish( + self.context(), """\ +.. rql-table:: + + %(rql)s + """ % {'rql': rql}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_vid(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + vid = 'mytable' + out = rest_publish( + self.context(), """\ +.. rql-table:: + :vid: %(vid)s + + %(rql)s + """ % {'rql': rql, 'vid': vid}) + req = self.request() + view = self.vreg['views'].select(vid, req, rset=req.execute(rql)) + self.assertEqual(view.render(w=None)[49:], out[49:]) + self.assertIn(vid, out[:49]) + + def test_rqltable_badvid(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + vid = 'mytabel' + out = rest_publish( + self.context(), """\ +.. rql-table:: + :vid: %(vid)s + + %(rql)s + """ % {'rql': rql, 'vid': vid}) + self.assertIn("fail to select '%s' view" % vid, out) + + def test_rqltable_headers(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + headers = ["nom", "prenom", "identifiant"] + out = rest_publish( + self.context(), """\ +.. rql-table:: + :headers: %(headers)s + + %(rql)s + """ % {'rql': rql, 'headers': ', '.join(headers)}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.headers = headers + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_headers_missing(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + headers = ["nom", "", "identifiant"] + out = rest_publish( + self.context(), """\ +.. rql-table:: + :headers: %(headers)s + + %(rql)s + """ % {'rql': rql, 'headers': ', '.join(headers)}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.headers = [headers[0], None, headers[2]] + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_headers_missing_edges(self): + rql = """Any S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + headers = [" ", "prenom", ""] + out = rest_publish( + self.context(), """\ +.. rql-table:: + :headers: %(headers)s + + %(rql)s + """ % {'rql': rql, 'headers': ', '.join(headers)}) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.headers = [None, headers[1], None] + self.assertEqual(view.render(w=None)[49:], out[49:]) + + def test_rqltable_colvids(self): + rql = """Any X,S,F,L WHERE X is CWUser, X surname S, + X firstname F, X login L""" + colvids = {0: "oneline"} + out = rest_publish( + self.context(), """\ +.. rql-table:: + :colvids: %(colvids)s + + %(rql)s + """ % {'rql': rql, + 'colvids': ', '.join(["%d=%s" % (k, v) + for k, v in colvids.iteritems()]) + }) + req = self.request() + view = self.vreg['views'].select('table', req, rset=req.execute(rql)) + view.cellvids = colvids + self.assertEqual(view.render(w=None)[49:], out[49:]) + + if __name__ == '__main__': unittest_main() diff -r cf27006ce813 -r b1e933b0e850 hooks/metadata.py --- a/hooks/metadata.py Fri Dec 06 17:20:59 2013 +0100 +++ b/hooks/metadata.py Mon Dec 09 16:13:10 2013 +0100 @@ -149,7 +149,7 @@ # entity source handling ####################################################### -class ChangeEntityUpdateCaches(hook.Operation): +class ChangeEntitySourceUpdateCaches(hook.Operation): oldsource = newsource = entity = None # make pylint happy def postcommit_event(self): @@ -221,6 +221,6 @@ 'mtime': datetime.now()} self._cw.system_sql(syssource.sqlgen.insert('entities', attrs), attrs) # register an operation to update repository/sources caches - ChangeEntityUpdateCaches(self._cw, entity=entity, - oldsource=oldsource.repo_source, - newsource=syssource) + ChangeEntitySourceUpdateCaches(self._cw, entity=entity, + oldsource=oldsource.repo_source, + newsource=syssource) diff -r cf27006ce813 -r b1e933b0e850 hooks/security.py --- a/hooks/security.py Fri Dec 06 17:20:59 2013 +0100 +++ b/hooks/security.py Mon Dec 09 16:13:10 2013 +0100 @@ -111,17 +111,11 @@ events = ('after_update_entity',) def __call__(self): - try: - # check user has permission right now, if not retry at commit time - self.entity.cw_check_perm('update') - check_entity_attributes(self._cw, self.entity) - except Unauthorized: - self.entity._cw_clear_local_perm_cache('update') - # save back editedattrs in case the entity is reedited later in the - # same transaction, which will lead to cw_edited being - # overwritten - CheckEntityPermissionOp.get_instance(self._cw).add_data( - (self.entity.eid, 'update', self.entity.cw_edited) ) + # save back editedattrs in case the entity is reedited later in the + # same transaction, which will lead to cw_edited being + # overwritten + CheckEntityPermissionOp.get_instance(self._cw).add_data( + (self.entity.eid, 'update', self.entity.cw_edited) ) class BeforeDelEntitySecurityHook(SecurityHook): diff -r cf27006ce813 -r b1e933b0e850 hooks/syncschema.py --- a/hooks/syncschema.py Fri Dec 06 17:20:59 2013 +0100 +++ b/hooks/syncschema.py Mon Dec 09 16:13:10 2013 +0100 @@ -28,7 +28,7 @@ from copy import copy from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema -from yams import buildobjs as ybo, schema2sql as y2sql +from yams import buildobjs as ybo, schema2sql as y2sql, convert_default_value from logilab.common.decorators import clear_cache @@ -39,21 +39,6 @@ from cubicweb.server import hook, schemaserial as ss from cubicweb.server.sqlutils import SQL_PREFIX - -TYPE_CONVERTER = { # XXX - 'Boolean': bool, - 'Int': int, - 'BigInt': int, - 'Float': float, - 'Password': str, - 'String': unicode, - 'Date' : unicode, - 'Datetime' : unicode, - 'Time' : unicode, - 'TZDatetime' : unicode, - 'TZTime' : unicode, - } - # core entity and relation types which can't be removed CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set( ('CWUser', 'CWGroup','login', 'upassword', 'name', 'in_group')) @@ -437,11 +422,11 @@ def precommit_event(self): session = self.session entity = self.entity - # entity.defaultval is a string or None, but we need a correctly typed + # entity.defaultval is a Binary or None, but we need a correctly typed # value default = entity.defaultval if default is not None: - default = TYPE_CONVERTER[entity.otype.name](default) + default = default.unzpickle() props = {'default': default, 'indexed': entity.indexed, 'fulltextindexed': entity.fulltextindexed, @@ -493,20 +478,11 @@ # attribute is still set to False, so we've to ensure it's False rschema.final = True insert_rdef_on_subclasses(session, eschema, rschema, rdefdef, props) - # set default value, using sql for performance and to avoid - # modification_date update - if default: - if rdefdef.object in ('Date', 'Datetime', 'TZDatetime'): - # XXX may may want to use creation_date - if default == 'TODAY': - default = syssource.dbhelper.sql_current_date() - elif default == 'NOW': - default = syssource.dbhelper.sql_current_timestamp() - session.system_sql('UPDATE %s SET %s=%s' - % (table, column, default)) - else: - session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column), - {'default': default}) + # update existing entities with the default value of newly added attribute + if default is not None: + default = convert_default_value(self.rdefdef, default) + session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column), + {'default': default}) def revertprecommit_event(self): # revert changes on in memory schema diff -r cf27006ce813 -r b1e933b0e850 hooks/test/unittest_syncschema.py --- a/hooks/test/unittest_syncschema.py Fri Dec 06 17:20:59 2013 +0100 +++ b/hooks/test/unittest_syncschema.py Mon Dec 09 16:13:10 2013 +0100 @@ -19,7 +19,7 @@ from logilab.common.testlib import TestCase, unittest_main -from cubicweb import ValidationError +from cubicweb import ValidationError, Binary from cubicweb.schema import META_RTYPES from cubicweb.devtools.testlib import CubicWebTC from cubicweb.server.sqlutils import SQL_PREFIX @@ -74,9 +74,10 @@ self.commit() self.assertTrue(schema.has_entity('Societe2')) self.assertTrue(schema.has_relation('concerne2')) - attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval "noname", ' + attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval %(default)s, ' ' X indexed TRUE, X relation_type RT, X from_entity E, X to_entity F ' - 'WHERE RT name "name", E name "Societe2", F name "String"')[0][0] + 'WHERE RT name "name", E name "Societe2", F name "String"', + {'default': Binary.zpickle('noname')})[0][0] self._set_attr_perms(attreid) concerne2_rdef_eid = self.execute( 'INSERT CWRelation X: X cardinality "**", X relation_type RT, X from_entity E, X to_entity E ' @@ -290,8 +291,10 @@ def test_add_attribute_to_base_class(self): - attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval "noname", X indexed TRUE, X relation_type RT, X from_entity E, X to_entity F ' - 'WHERE RT name "messageid", E name "BaseTransition", F name "String"')[0][0] + attreid = self.execute('INSERT CWAttribute X: X cardinality "11", X defaultval %(default)s, ' + 'X indexed TRUE, X relation_type RT, X from_entity E, X to_entity F ' + 'WHERE RT name "messageid", E name "BaseTransition", F name "String"', + {'default': Binary.zpickle('noname')})[0][0] assert self.execute('SET X read_permission Y WHERE X eid %(x)s, Y name "managers"', {'x': attreid}) self.commit() diff -r cf27006ce813 -r b1e933b0e850 interfaces.py --- a/interfaces.py Fri Dec 06 17:20:59 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,214 +0,0 @@ -# copyright 2003-2010 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 . -"""Standard interfaces. Deprecated in favor of adapters. - -.. note:: - - The `implements` selector used to match not only entity classes but also their - interfaces. This will disappear in a future version. You should define an - adapter for that interface and use `adaptable('MyIFace')` selector on appobjects - that require that interface. - -""" -__docformat__ = "restructuredtext en" - -from logilab.common.interface import Interface - - -# XXX deprecates in favor of IProgressAdapter -class IProgress(Interface): - """something that has a cost, a state and a progression""" - - @property - def cost(self): - """the total cost""" - - @property - def done(self): - """what is already done""" - - @property - def todo(self): - """what remains to be done""" - - def progress_info(self): - """returns a dictionary describing progress/estimated cost of the - version. - - - mandatory keys are (''estimated', 'done', 'todo') - - - optional keys are ('notestimated', 'notestimatedcorrected', - 'estimatedcorrected') - - 'noestimated' and 'notestimatedcorrected' should default to 0 - 'estimatedcorrected' should default to 'estimated' - """ - - def finished(self): - """returns True if status is finished""" - - def in_progress(self): - """returns True if status is not finished""" - - def progress(self): - """returns the % progress of the task item""" - -# XXX deprecates in favor of IMileStoneAdapter -class IMileStone(IProgress): - """represents an ITask's item""" - - parent_type = None # specify main task's type - - def get_main_task(self): - """returns the main ITask entity""" - - def initial_prevision_date(self): - """returns the initial expected end of the milestone""" - - def eta_date(self): - """returns expected date of completion based on what remains - to be done - """ - - def completion_date(self): - """returns date on which the subtask has been completed""" - - def contractors(self): - """returns the list of persons supposed to work on this task""" - -# XXX deprecates in favor of IEmbedableAdapter -class IEmbedable(Interface): - """interface for embedable entities""" - - def embeded_url(self): - """embed action interface""" - -# XXX deprecates in favor of ICalendarViewsAdapter -class ICalendarViews(Interface): - """calendar views interface""" - def matching_dates(self, begin, end): - """ - :param begin: day considered as begin of the range (`DateTime`) - :param end: day considered as end of the range (`DateTime`) - - :return: - a list of dates (`DateTime`) in the range [`begin`, `end`] on which - this entity apply - """ - -# XXX deprecates in favor of ICalendarableAdapter -class ICalendarable(Interface): - """interface for items that do have a begin date 'start' and an end date 'stop' - """ - - @property - def start(self): - """return start date""" - - @property - def stop(self): - """return stop state""" - -# XXX deprecates in favor of ICalendarableAdapter -class ITimetableViews(Interface): - """timetable views interface""" - def timetable_date(self): - """XXX explain - - :return: date (`DateTime`) - """ - -# XXX deprecates in favor of IGeocodableAdapter -class IGeocodable(Interface): - """interface required by geocoding views such as gmap-view""" - - @property - def latitude(self): - """returns the latitude of the entity""" - - @property - def longitude(self): - """returns the longitude of the entity""" - - def marker_icon(self): - """returns the icon that should be used as the marker""" - - -# XXX deprecates in favor of IEmailableAdapter -class IFeed(Interface): - """interface for entities with rss flux""" - - def rss_feed_url(self): - """""" - -# XXX deprecates in favor of IDownloadableAdapter -class IDownloadable(Interface): - """interface for downloadable entities""" - - def download_url(self): # XXX not really part of this interface - """return an url to download entity's content""" - def download_content_type(self): - """return MIME type of the downloadable content""" - def download_encoding(self): - """return encoding of the downloadable content""" - def download_file_name(self): - """return file name of the downloadable content""" - def download_data(self): - """return actual data of the downloadable content""" - -# XXX deprecates in favor of IPrevNextAdapter -class IPrevNext(Interface): - """interface for entities which can be linked to a previous and/or next - entity - """ - - def next_entity(self): - """return the 'next' entity""" - def previous_entity(self): - """return the 'previous' entity""" - -# XXX deprecates in favor of IBreadCrumbsAdapter -class IBreadCrumbs(Interface): - - def breadcrumbs(self, view, recurs=False): - pass - -# XXX deprecates in favor of ITreeAdapter -class ITree(Interface): - - def parent(self): - """returns the parent entity""" - - def children(self): - """returns the item's children""" - - def children_rql(self): - """XXX returns RQL to get children""" - - def iterchildren(self): - """iterates over the item's children""" - - def is_leaf(self): - """returns true if this node as no child""" - - def is_root(self): - """returns true if this node has no parent""" - - def root(self): - """returns the root object""" - diff -r cf27006ce813 -r b1e933b0e850 migration.py --- a/migration.py Fri Dec 06 17:20:59 2013 +0100 +++ b/migration.py Mon Dec 09 16:13:10 2013 +0100 @@ -257,7 +257,7 @@ home_key = 'HOME' if sys.platform == 'win32': home_key = 'USERPROFILE' - histfile = os.path.join(os.environ[home_key], ".eshellhist") + histfile = os.path.join(os.environ[home_key], ".cwshell_history") try: readline.read_history_file(histfile) except IOError: diff -r cf27006ce813 -r b1e933b0e850 misc/migration/3.18.0_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.18.0_Any.py Mon Dec 09 16:13:10 2013 +0100 @@ -0,0 +1,83 @@ +sync_schema_props_perms('defaultval') + +def convert_defaultval(cwattr, default): + from decimal import Decimal + import yams + from cubicweb import Binary + if default is None: + return + atype = cwattr.to_entity[0].name + if atype == 'Boolean': + assert default in ('True', 'False'), default + default = default == 'True' + elif atype in ('Int', 'BigInt'): + default = int(default) + elif atype == 'Float': + default = float(default) + elif atype == 'Decimal': + default = Decimal(default) + elif atype in ('Date', 'Datetime', 'TZDatetime', 'Time'): + try: + # handle NOW and TODAY, keep them stored as strings + yams.KEYWORD_MAP[atype][default.upper()] + default = default.upper() + except KeyError: + # otherwise get an actual date or datetime + default = yams.DATE_FACTORY_MAP[atype](default) + else: + assert atype == 'String', atype + default = unicode(default) + return Binary.zpickle(default) + +dbh = repo.system_source.dbhelper +driver = config.sources()['system']['db-driver'] + +if driver == 'postgres' or driver.startswith('sqlserver'): + + sql('ALTER TABLE cw_cwattribute ADD new_defaultval %s' % dbh.TYPE_MAPPING['Bytes']) + + for cwattr in rql('CWAttribute X').entities(): + olddefault = cwattr.defaultval + if olddefault is not None: + req = "UPDATE cw_cwattribute SET new_defaultval = %(val)s WHERE cw_eid = %(eid)s" + args = {'val': dbh.binary_value(convert_defaultval(cwattr, olddefault).getvalue()), 'eid': cwattr.eid} + sql(req, args, ask_confirm=False) + + sql('ALTER TABLE cw_cwattribute DROP COLUMN cw_defaultval') + if config.sources()['system']['db-driver'] == 'postgres': + sql('ALTER TABLE cw_cwattribute RENAME COLUMN new_defaultval TO cw_defaultval') + else: + sql("sp_rename 'cw_cwattribute.new_defaultval', 'cw_defaultval', 'COLUMN'") + +elif driver == 'sqlite': + + import re + create = sql("SELECT sql FROM sqlite_master WHERE name = 'cw_CWAttribute'")[0][0] + create = re.sub('cw_defaultval varchar[^,]*,', 'cw_defaultval bytea,', create, re.I) + create = re.sub('cw_CWAttribute', 'tmp_cw_CWAttribute', create, re.I) + sql(create) + sql("INSERT INTO tmp_cw_CWAttribute SELECT * FROM cw_CWAttribute") + for cwattr in rql('CWAttribute X').entities(): + olddefault = cwattr.defaultval + if olddefault is None: + continue + req = "UPDATE tmp_cw_CWAttribute SET cw_defaultval = %(val)s WHERE cw_eid = %(eid)s" + args = {'val': dbh.binary_value(convert_defaultval(cwattr, olddefault).getvalue()), + 'eid': cwattr.eid} + sql(req, args, ask_confirm=False) + + sql('DROP TABLE cw_CWAttribute') + sql('ALTER TABLE tmp_cw_CWAttribute RENAME TO cw_CWAttribute') + +else: + assert False, 'upgrade not supported on this database backend' + +# Set object type to "Bytes" for CWAttribute's "defaultval" attribute +rql('SET X to_entity B WHERE X is CWAttribute, X from_entity Y, Y name "CWAttribute", ' + 'X relation_type Z, Z name "defaultval", B name "Bytes"') + +from yams import buildobjs as ybo +schema.add_relation_def(ybo.RelationDefinition('CWAttribute', 'defaultval', 'Bytes')) +schema.del_relation_def('CWAttribute', 'defaultval', 'String') + +commit() diff -r cf27006ce813 -r b1e933b0e850 mixins.py --- a/mixins.py Fri Dec 06 17:20:59 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,308 +0,0 @@ -# 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 . -"""mixins of entity/views organized somewhat in a graph or tree structure""" -__docformat__ = "restructuredtext en" - -from itertools import chain - -from logilab.common.decorators import cached -from logilab.common.deprecation import deprecated, class_deprecated - -from cubicweb.predicates import implements -from cubicweb.interfaces import ITree - - -class TreeMixIn(object): - """base tree-mixin implementing the tree interface - - This mixin has to be inherited explicitly and configured using the - tree_attribute, parent_target and children_target class attribute to - benefit from this default implementation - """ - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] TreeMixIn is deprecated, use/override ITreeAdapter instead (%(cls)s)' - - tree_attribute = None - # XXX misnamed - parent_target = 'subject' - children_target = 'object' - - def different_type_children(self, entities=True): - """return children entities of different type as this entity. - - according to the `entities` parameter, return entity objects or the - equivalent result set - """ - res = self.related(self.tree_attribute, self.children_target, - entities=entities) - if entities: - return [e for e in res if e.e_schema != self.e_schema] - return res.filtered_rset(lambda x: x.e_schema != self.e_schema, self.cw_col) - - def same_type_children(self, entities=True): - """return children entities of the same type as this entity. - - according to the `entities` parameter, return entity objects or the - equivalent result set - """ - res = self.related(self.tree_attribute, self.children_target, - entities=entities) - if entities: - return [e for e in res if e.e_schema == self.e_schema] - return res.filtered_rset(lambda x: x.e_schema is self.e_schema, self.cw_col) - - def iterchildren(self, _done=None): - if _done is None: - _done = set() - for child in self.children(): - if child.eid in _done: - self.error('loop in %s tree: %s', self.__regid__.lower(), child) - continue - yield child - _done.add(child.eid) - - def prefixiter(self, _done=None): - if _done is None: - _done = set() - if self.eid in _done: - return - _done.add(self.eid) - yield self - for child in self.same_type_children(): - for entity in child.prefixiter(_done): - yield entity - - @cached - def path(self): - """returns the list of eids from the root object to this object""" - path = [] - parent = self - while parent: - if parent.eid in path: - self.error('loop in %s tree: %s', self.__regid__.lower(), parent) - break - path.append(parent.eid) - try: - # check we are not leaving the tree - if (parent.tree_attribute != self.tree_attribute or - parent.parent_target != self.parent_target): - break - parent = parent.parent() - except AttributeError: - break - - path.reverse() - return path - - def iterparents(self, strict=True): - def _uptoroot(self): - curr = self - while True: - curr = curr.parent() - if curr is None: - break - yield curr - if not strict: - return chain([self], _uptoroot(self)) - return _uptoroot(self) - - ## ITree interface ######################################################## - def parent(self): - """return the parent entity if any, else None (e.g. if we are on the - root - """ - try: - return self.related(self.tree_attribute, self.parent_target, - entities=True)[0] - except (KeyError, IndexError): - return None - - def children(self, entities=True, sametype=False): - """return children entities - - according to the `entities` parameter, return entity objects or the - equivalent result set - """ - if sametype: - return self.same_type_children(entities) - else: - return self.related(self.tree_attribute, self.children_target, - entities=entities) - - def children_rql(self): - return self.cw_related_rql(self.tree_attribute, self.children_target) - - def is_leaf(self): - return len(self.children()) == 0 - - def is_root(self): - return self.parent() is None - - def root(self): - """return the root object""" - return self._cw.entity_from_eid(self.path()[0]) - - -class EmailableMixIn(object): - """base mixin providing the default get_email() method used by - the massmailing view - - NOTE: The default implementation is based on the - primary_email / use_email scheme - """ - @deprecated("[3.9] use entity.cw_adapt_to('IEmailable').get_email()") - def get_email(self): - if getattr(self, 'primary_email', None): - return self.primary_email[0].address - if getattr(self, 'use_email', None): - return self.use_email[0].address - return None - - -"""pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity -classes which have the relation described by the dict's key. - -NOTE: pluggable mixins can't override any method of the 'explicit' user classes tree -(eg without plugged classes). This includes bases Entity and AnyEntity classes. -""" -MI_REL_TRIGGERS = { - ('primary_email', 'subject'): EmailableMixIn, - ('use_email', 'subject'): EmailableMixIn, - } - - -# XXX move to cubicweb.web.views.treeview once we delete usage from this file -def _done_init(done, view, row, col): - """handle an infinite recursion safety belt""" - if done is None: - done = set() - entity = view.cw_rset.get_entity(row, col) - if entity.eid in done: - msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % { - 'rel': entity.cw_adapt_to('ITree').tree_relation, - 'eid': entity.eid - } - return None, msg - done.add(entity.eid) - return done, entity - - -class TreeViewMixIn(object): - """a recursive tree view""" - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] TreeViewMixIn is deprecated, use/override BaseTreeView instead (%(cls)s)' - - __regid__ = 'tree' - __select__ = implements(ITree, warn=False) - item_vid = 'treeitem' - - def call(self, done=None, **kwargs): - if done is None: - done = set() - super(TreeViewMixIn, self).call(done=done, **kwargs) - - def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, **kwargs): - assert maxlevel is None or maxlevel > 0 - done, entity = _done_init(done, self, row, col) - if done is None: - # entity is actually an error message - self.w(u'
  • %s
  • ' % entity) - return - self.open_item(entity) - entity.view(vid or self.item_vid, w=self.w, **kwargs) - if maxlevel is not None: - maxlevel -= 1 - if maxlevel == 0: - self.close_item(entity) - return - relatedrset = entity.children(entities=False) - self.wview(self.__regid__, relatedrset, 'null', done=done, - maxlevel=maxlevel, **kwargs) - self.close_item(entity) - - def open_item(self, entity): - self.w(u'
  • \n' % entity.cw_etype.lower()) - def close_item(self, entity): - self.w(u'
  • \n') - - -class TreePathMixIn(object): - """a recursive path view""" - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] TreePathMixIn is deprecated, use/override TreePathView instead (%(cls)s)' - __regid__ = 'path' - item_vid = 'oneline' - separator = u' > ' - - def call(self, **kwargs): - self.w(u'
    ') - super(TreePathMixIn, self).call(**kwargs) - self.w(u'
    ') - - def cell_call(self, row, col=0, vid=None, done=None, **kwargs): - done, entity = _done_init(done, self, row, col) - if done is None: - # entity is actually an error message - self.w(u'%s' % entity) - return - parent = entity.parent() - if parent: - parent.view(self.__regid__, w=self.w, done=done) - self.w(self.separator) - entity.view(vid or self.item_vid, w=self.w) - - -class ProgressMixIn(object): - """provide a default implementations for IProgress interface methods""" - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] ProgressMixIn is deprecated, use/override IProgressAdapter instead (%(cls)s)' - - @property - def cost(self): - return self.progress_info()['estimated'] - - @property - def revised_cost(self): - return self.progress_info().get('estimatedcorrected', self.cost) - - @property - def done(self): - return self.progress_info()['done'] - - @property - def todo(self): - return self.progress_info()['todo'] - - @cached - def progress_info(self): - raise NotImplementedError() - - def finished(self): - return not self.in_progress() - - def in_progress(self): - raise NotImplementedError() - - def progress(self): - try: - return 100. * self.done / self.revised_cost - except ZeroDivisionError: - # total cost is 0 : if everything was estimated, task is completed - if self.progress_info().get('notestimated'): - return 0. - return 100 diff -r cf27006ce813 -r b1e933b0e850 predicates.py --- a/predicates.py Fri Dec 06 17:20:59 2013 +0100 +++ b/predicates.py Mon Dec 09 16:13:10 2013 +0100 @@ -204,27 +204,6 @@ # remember, these imports are there for bw compat only __BACKWARD_COMPAT_IMPORTS = (yes,) -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 ########################################### @@ -745,53 +724,6 @@ 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 = {} diff -r cf27006ce813 -r b1e933b0e850 schema.py --- a/schema.py Fri Dec 06 17:20:59 2013 +0100 +++ b/schema.py Mon Dec 09 16:13:10 2013 +0100 @@ -44,6 +44,15 @@ import cubicweb from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized +try: + from cubicweb import server +except ImportError: + # We need to lookup DEBUG from there, + # however a pure dbapi client may not have it. + class server(object): pass + server.DEBUG = False + + PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',)) VIRTUAL_RTYPES = set(('eid', 'identity', 'has_text',)) @@ -268,13 +277,25 @@ return False PermissionMixIn.has_perm = has_perm + def check_perm(self, _cw, action, **kwargs): # NB: _cw may be a server transaction or a request object. # # check user is in an allowed group, if so that's enough internal # transactions should always stop there + DBG = False + if server.DEBUG & server.DBG_SEC: + if action in server._SECURITY_CAPS: + _self_str = str(self) + if server._SECURITY_ITEMS: + if any(item in _self_str for item in server._SECURITY_ITEMS): + DBG = True + else: + DBG = True groups = self.get_groups(action) if _cw.user.matching_groups(groups): + if DBG: + print 'check_perm: %r %r: user matches %s' % (action, _self_str, groups) return # if 'owners' in allowed groups, check if the user actually owns this # object, if so that's enough @@ -284,8 +305,15 @@ if 'owners' in groups and ( kwargs.get('creating') or ('eid' in kwargs and _cw.user.owns(kwargs['eid']))): + if DBG: + print ('check_perm: %r %r: user is owner or creation time' % + (action, _self_str)) return # else if there is some rql expressions, check them + if DBG: + print ('check_perm: %r %r %s' % + (action, _self_str, [(rqlexpr, kwargs, rqlexpr.check(_cw, **kwargs)) + for rqlexpr in self.get_rqlexprs(action)])) if any(rqlexpr.check(_cw, **kwargs) for rqlexpr in self.get_rqlexprs(action)): return @@ -933,14 +961,11 @@ distinct_query = None def serialize(self): - # start with a comma for bw compat,see below + # start with a semicolon for bw compat, see below return ';' + ','.join(sorted(self.mainvars)) + ';' + self.expression @classmethod def deserialize(cls, value): - # XXX < 3.5.10 bw compat - if not value.startswith(';'): - return cls(value) _, mainvars, expression = value.split(';', 2) return cls(expression, mainvars) @@ -995,9 +1020,6 @@ self.msg or '') def deserialize(cls, value): - # XXX < 3.5.10 bw compat - if not value.startswith(';'): - return cls(value) value, msg = value.split('\n', 1) _, mainvars, expression = value.split(';', 2) return cls(expression, mainvars, msg) diff -r cf27006ce813 -r b1e933b0e850 schemas/bootstrap.py --- a/schemas/bootstrap.py Fri Dec 06 17:20:59 2013 +0100 +++ b/schemas/bootstrap.py Mon Dec 09 16:13:10 2013 +0100 @@ -83,7 +83,7 @@ indexed = Boolean(description=_('create an index for quick search on this attribute')) fulltextindexed = Boolean(description=_('index this attribute\'s value in the plain text index')) internationalizable = Boolean(description=_('is this attribute\'s value translatable')) - defaultval = String(maxsize=256) + defaultval = Bytes(description=_('default value as gziped pickled python object')) extra_props = Bytes(description=_('additional type specific properties')) description = RichString(internationalizable=True, diff -r cf27006ce813 -r b1e933b0e850 server/__init__.py --- a/server/__init__.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/__init__.py Mon Dec 09 16:13:10 2013 +0100 @@ -26,6 +26,7 @@ import sys from os.path import join, exists from glob import glob +from contextlib import contextmanager from logilab.common.modutils import LazyObject from logilab.common.textutils import splitstrip @@ -80,14 +81,57 @@ DBG_HOOKS = 16 #: operations DBG_OPS = 32 +#: security +DBG_SEC = 64 #: more verbosity -DBG_MORE = 64 +DBG_MORE = 128 #: all level enabled -DBG_ALL = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_MORE +DBG_ALL = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_SEC + DBG_MORE + +_SECURITY_ITEMS = [] +_SECURITY_CAPS = ['read', 'add', 'update', 'delete'] #: current debug mode DEBUG = 0 +@contextmanager +def tunesecurity(items=(), capabilities=()): + """Context manager to use in conjunction with DBG_SEC. + + This allows some tuning of: + * the monitored capabilities ('read', 'add', ....) + * the object being checked by the security checkers + + When no item is given, all of them will be watched. + By default all capabilities are monitored, unless specified. + + Example use:: + + from cubicweb.server import debugged, DBG_SEC, tunesecurity + with debugged(DBG_SEC): + with tunesecurity(items=('Elephant', 'trumps'), + capabilities=('update', 'delete')): + babar.cw_set(trumps=celeste) + flore.cw_delete() + + ==> + + check_perm: 'update' 'relation Elephant.trumps.Elephant' + [(ERQLExpression(Any X WHERE U has_update_permission X, X eid %(x)s, U eid %(u)s), + {'eid': 2167}, True)] + check_perm: 'delete' 'Elephant' + [(ERQLExpression(Any X WHERE U has_delete_permission X, X eid %(x)s, U eid %(u)s), + {'eid': 2168}, True)] + + """ + olditems = _SECURITY_ITEMS[:] + _SECURITY_ITEMS.extend(list(items)) + oldactions = _SECURITY_CAPS[:] + _SECURITY_CAPS[:] = capabilities + yield + _SECURITY_ITEMS[:] = olditems + _SECURITY_CAPS[:] = oldactions + def set_debug(debugmode): """change the repository debugging mode""" global DEBUG @@ -309,7 +353,6 @@ SOURCE_TYPES = {'native': LazyObject('cubicweb.server.sources.native', 'NativeSQLSource'), 'datafeed': LazyObject('cubicweb.server.sources.datafeed', 'DataFeedSource'), 'ldapfeed': LazyObject('cubicweb.server.sources.ldapfeed', 'LDAPFeedSource'), - 'ldapuser': LazyObject('cubicweb.server.sources.ldapuser', 'LDAPUserSource'), 'pyrorql': LazyObject('cubicweb.server.sources.pyrorql', 'PyroRQLSource'), 'zmqrql': LazyObject('cubicweb.server.sources.zmqrql', 'ZMQRQLSource'), } diff -r cf27006ce813 -r b1e933b0e850 server/hook.py --- a/server/hook.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/hook.py Mon Dec 09 16:13:10 2013 +0100 @@ -678,16 +678,6 @@ {'x': self.eidfrom, 'p': self.eidto}) -PropagateSubjectRelationHook = class_renamed( - 'PropagateSubjectRelationHook', PropagateRelationHook, - '[3.9] PropagateSubjectRelationHook has been renamed to PropagateRelationHook') -PropagateSubjectRelationAddHook = class_renamed( - 'PropagateSubjectRelationAddHook', PropagateRelationAddHook, - '[3.9] PropagateSubjectRelationAddHook has been renamed to PropagateRelationAddHook') -PropagateSubjectRelationDelHook = class_renamed( - 'PropagateSubjectRelationDelHook', PropagateRelationDelHook, - '[3.9] PropagateSubjectRelationDelHook has been renamed to PropagateRelationDelHook') - # abstract classes for operation ############################################### diff -r cf27006ce813 -r b1e933b0e850 server/migractions.py --- a/server/migractions.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/migractions.py Mon Dec 09 16:13:10 2013 +0100 @@ -1461,12 +1461,9 @@ # no result to fetch return - def rqlexec(self, rql, kwargs=None, cachekey=None, build_descr=True, + def rqlexec(self, rql, kwargs=None, build_descr=True, ask_confirm=False): """rql action""" - if cachekey is not None: - warn('[3.8] cachekey is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) if not isinstance(rql, (tuple, list)): rql = ( (rql, kwargs), ) res = None diff -r cf27006ce813 -r b1e933b0e850 server/msplanner.py --- a/server/msplanner.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/msplanner.py Mon Dec 09 16:13:10 2013 +0100 @@ -92,6 +92,7 @@ from logilab.common.compat import any from logilab.common.decorators import cached +from logilab.common.deprecation import deprecated from rql import BadRQLQuery from rql.stmts import Union, Select @@ -1262,6 +1263,7 @@ inputmap.update(step.outputmap) +@deprecated('[3.18] old multi-source system will go away in the next version') class MSPlanner(SSPlanner): """MultiSourcesPlanner: build execution plan for rql queries diff -r cf27006ce813 -r b1e933b0e850 server/querier.py --- a/server/querier.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/querier.py Mon Dec 09 16:13:10 2013 +0100 @@ -85,6 +85,7 @@ # use `term_etype` since we've to deal with rewritten constants here, # when used as an external source by another repository. # XXX what about local read security w/ those rewritten constants... + DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS schema = session.repo.schema if rqlst.where is not None: for rel in rqlst.where.iget_nodes(Relation): @@ -102,8 +103,14 @@ term_etype(session, rel.children[1].children[0], solution, args)) if not session.user.matching_groups(rdef.get_groups('read')): + if DBG: + print ('check_read_access: %s %s does not match %s' % + (rdef, session.user.groups, rdef.get_groups('read'))) # XXX rqlexpr not allowed raise Unauthorized('read', rel.r_type) + if DBG: + print ('check_read_access: %s %s matches %s' % + (rdef, session.user.groups, rdef.get_groups('read'))) localchecks = {} # iterate on defined_vars and not on solutions to ignore column aliases for varname in rqlst.defined_vars: @@ -115,6 +122,9 @@ if not erqlexprs: ex = Unauthorized('read', solution[varname]) ex.var = varname + if DBG: + print ('check_read_access: %s %s %s %s' % + (varname, eschema, session.user.groups, eschema.get_groups('read'))) raise ex # don't insert security on variable only referenced by 'NOT X relation Y' or # 'NOT EXISTS(X relation Y)' diff -r cf27006ce813 -r b1e933b0e850 server/repository.py --- a/server/repository.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/repository.py Mon Dec 09 16:13:10 2013 +0100 @@ -36,6 +36,7 @@ from os.path import join from datetime import datetime from time import time, localtime, strftime +from warnings import warn from logilab.common.decorators import cached, clear_cache from logilab.common.compat import any @@ -299,6 +300,8 @@ # initialized) source.init(True, sourceent) if not source.copy_based_source: + warn('[3.18] old multi-source system will go away in the next version', + DeprecationWarning) self.sources.append(source) self.querier.set_planner() if add_to_cnxsets: @@ -1665,18 +1668,24 @@ @cached def rel_type_sources(self, rtype): + warn('[3.18] old multi-source system will go away in the next version', + DeprecationWarning) return tuple([source for source in self.sources if source.support_relation(rtype) or rtype in source.dont_cross_relations]) @cached def can_cross_relation(self, rtype): + warn('[3.18] old multi-source system will go away in the next version', + DeprecationWarning) return tuple([source for source in self.sources if source.support_relation(rtype) and rtype in source.cross_relations]) @cached def is_multi_sources_relation(self, rtype): + warn('[3.18] old multi-source system will go away in the next version', + DeprecationWarning) return any(source for source in self.sources if not source is self.system_source and source.support_relation(rtype)) diff -r cf27006ce813 -r b1e933b0e850 server/schemaserial.py --- a/server/schemaserial.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/schemaserial.py Mon Dec 09 16:13:10 2013 +0100 @@ -27,7 +27,7 @@ from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo -from cubicweb import CW_SOFTWARE_ROOT, Binary +from cubicweb import CW_SOFTWARE_ROOT, Binary, typed_eid from cubicweb.schema import (KNOWN_RPROPERTIES, CONSTRAINTS, ETYPE_NAME_MAP, VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES) from cubicweb.server import sqlutils @@ -77,8 +77,6 @@ def cstrtype_mapping(cursor): """cached constraint types mapping""" map = dict(cursor.execute('Any T, X WHERE X is CWConstraintType, X name T')) - if not 'BoundConstraint' in map: - map['BoundConstraint'] = map['BoundaryConstraint'] return map # schema / perms deserialization ############################################## @@ -214,6 +212,11 @@ rdefeid, seid, reid, oeid, card, ord, desc, idx, ftidx, i18n, default = values typeparams = extra_props.get(rdefeid) typeparams = json.load(typeparams) if typeparams else {} + if default is not None: + if isinstance(default, Binary): + # while migrating from 3.17 to 3.18, we still have to + # handle String defaults + default = default.unzpickle() _add_rdef(rdefeid, seid, reid, oeid, cardinality=card, description=desc, order=ord, indexed=idx, fulltextindexed=ftidx, internationalizable=i18n, @@ -234,28 +237,24 @@ if rdefs is not None: set_perms(rdefs, permsidx) unique_togethers = {} - try: - rset = session.execute( - 'Any X,E,R WHERE ' - 'X is CWUniqueTogetherConstraint, ' - 'X constraint_of E, X relations R', build_descr=False) - except Exception: - session.rollback() # first migration introducing CWUniqueTogetherConstraint cw 3.9.6 - else: - for values in rset: - uniquecstreid, eeid, releid = values - eschema = schema.schema_by_eid(eeid) - relations = unique_togethers.setdefault(uniquecstreid, (eschema, [])) - rel = ertidx[releid] - if isinstance(rel, schemamod.RelationDefinitionSchema): - # not yet migrated 3.9 database ('relations' target type changed - # to CWRType in 3.10) - rtype = rel.rtype.type - else: - rtype = str(rel) - relations[1].append(rtype) - for eschema, unique_together in unique_togethers.itervalues(): - eschema._unique_together.append(tuple(sorted(unique_together))) + rset = session.execute( + 'Any X,E,R WHERE ' + 'X is CWUniqueTogetherConstraint, ' + 'X constraint_of E, X relations R', build_descr=False) + for values in rset: + uniquecstreid, eeid, releid = values + eschema = schema.schema_by_eid(eeid) + relations = unique_togethers.setdefault(uniquecstreid, (eschema, [])) + rel = ertidx[releid] + if isinstance(rel, schemamod.RelationDefinitionSchema): + # not yet migrated 3.9 database ('relations' target type changed + # to CWRType in 3.10) + rtype = rel.rtype.type + else: + rtype = str(rel) + relations[1].append(rtype) + for eschema, unique_together in unique_togethers.itervalues(): + eschema._unique_together.append(tuple(sorted(unique_together))) schema.infer_specialization_rules() session.commit() schema.reading_from_database = False @@ -344,13 +343,10 @@ cstrtypemap = {} rql = 'INSERT CWConstraintType X: X name %(ct)s' for cstrtype in CONSTRAINTS: - if cstrtype == 'BoundConstraint': - continue # XXX deprecated in yams 0.29 / cw 3.8.1 cstrtypemap[cstrtype] = execute(rql, {'ct': unicode(cstrtype)}, build_descr=False)[0][0] if pb is not None: pb.update() - cstrtypemap['BoundConstraint'] = cstrtypemap['BoundaryConstraint'] # serialize relations for rschema in schema.relations(): # skip virtual relations such as eid, has_text and identity @@ -536,10 +532,7 @@ elif isinstance(value, str): value = unicode(value) if value is not None and prop == 'default': - if value is False: - value = u'' - if not isinstance(value, unicode): - value = unicode(value) + value = Binary.zpickle(value) values[amap.get(prop, prop)] = value if extra: values['extra_props'] = Binary(json.dumps(extra)) diff -r cf27006ce813 -r b1e933b0e850 server/serverctl.py --- a/server/serverctl.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/serverctl.py Mon Dec 09 16:13:10 2013 +0100 @@ -1065,6 +1065,25 @@ if val: print key, ':', val + + +def permissionshandler(relation, perms): + from yams.schema import RelationDefinitionSchema + from yams.buildobjs import DEFAULT_ATTRPERMS + from cubicweb.schema import (PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS, + PUB_SYSTEM_ATTR_PERMS, RO_REL_PERMS, RO_ATTR_PERMS) + defaultrelperms = (DEFAULT_ATTRPERMS, PUB_SYSTEM_REL_PERMS, + PUB_SYSTEM_ATTR_PERMS, RO_REL_PERMS, RO_ATTR_PERMS) + defaulteperms = (PUB_SYSTEM_ENTITY_PERMS,) + # canonicalize vs str/unicode + for p in ('read', 'add', 'update', 'delete'): + rule = perms.get(p) + if rule: + perms[p] = tuple(str(x) if isinstance(x, basestring) else x + for x in rule) + return perms, perms in defaultrelperms or perms in defaulteperms + + class SchemaDiffCommand(Command): """Generate a diff between schema and fsschema description. @@ -1085,7 +1104,7 @@ repo, cnx = repo_cnx(config) session = repo._get_session(cnx.sessionid, setcnxset=True) fsschema = config.load_schema(expand_cubes=True) - schema_diff(repo.schema, fsschema, diff_tool) + schema_diff(fsschema, repo.schema, permissionshandler, diff_tool, ignore=('eid',)) for cmdclass in (CreateInstanceDBCommand, InitInstanceCommand, diff -r cf27006ce813 -r b1e933b0e850 server/session.py --- a/server/session.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/session.py Mon Dec 09 16:13:10 2013 +0100 @@ -1173,14 +1173,11 @@ source_from_eid = tx_meth('source_from_eid') - def execute(self, rql, kwargs=None, eid_key=None, build_descr=True): + def execute(self, rql, kwargs=None, build_descr=True): """db-api like method directly linked to the querier execute method. See :meth:`cubicweb.dbapi.Cursor.execute` documentation. """ - if eid_key is not None: - warn('[3.8] eid_key is deprecated, you can safely remove this argument', - DeprecationWarning, stacklevel=2) self.timestamp = time() # update timestamp rset = self._execute(self, rql, kwargs, build_descr) rset.req = self diff -r cf27006ce813 -r b1e933b0e850 server/sources/__init__.py --- a/server/sources/__init__.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/sources/__init__.py Mon Dec 09 16:13:10 2013 +0100 @@ -183,7 +183,7 @@ elif value is not None: # type check try: - value = configuration.convert(value, optdict, optname) + value = configuration._validate(value, optdict, optname) except Exception as ex: msg = unicode(ex) # XXX internationalization raise ValidationError(eid, {role_name('config', 'subject'): msg}) diff -r cf27006ce813 -r b1e933b0e850 server/sources/ldapuser.py --- a/server/sources/ldapuser.py Fri Dec 06 17:20:59 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,551 +0,0 @@ -# copyright 2003-2013 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 . -"""cubicweb ldap user source - -this source is for now limited to a read-only CWUser source -""" -from __future__ import division, with_statement -from base64 import b64decode - -import ldap -from ldap.filter import escape_filter_chars - -from rql.nodes import Relation, VariableRef, Constant, Function - -import warnings -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, - TimedCache) - -# 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, - } - - -# module is lazily imported -warnings.warn('Imminent drop of ldapuser. Switch to ldapfeed now!', - DeprecationWarning) - - -class LDAPUserSource(ldaputils.LDAPSourceMixIn, AbstractSource): - """LDAP read-only CWUser source""" - support_entities = {'CWUser': False} - - options = ldaputils.LDAPSourceMixIn.options + ( - - ('synchronization-interval', - {'type' : 'time', - 'default': '1d', - 'help': 'interval between synchronization with the ldap \ -directory (default to once a day).', - 'group': 'ldap-source', 'level': 3, - }), - ('cache-life-time', - {'type' : 'time', - 'default': '2h', - 'help': 'life time of query cache (default to two hours).', - 'group': 'ldap-source', 'level': 3, - }), - - ) - - def update_config(self, source_entity, typedconfig): - """update configuration from source entity. `typedconfig` is config - properly typed with defaults set - """ - super(LDAPUserSource, self).update_config(source_entity, typedconfig) - self._interval = typedconfig['synchronization-interval'] - self._cache_ttl = max(71, typedconfig['cache-life-time']) - self.reset_caches() - # 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""" - self._cache = {} - self._query_cache = TimedCache(self._cache_ttl) - - def init(self, activated, source_entity): - """method called by the repository once ready to handle request""" - super(LDAPUserSource, self).init(activated, source_entity) - if activated: - self.info('ldap init') - # set minimum period of 5min 1s (the additional second is to - # minimize resonnance effet) - 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): - with self.repo.internal_session() as session: - self.pull_data(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) - ldap_emailattr = self.user_rev_attrs['email'] - assert ldap_emailattr - execute = session.execute - cursor = session.system_sql("SELECT eid, extid FROM entities WHERE " - "source='%s'" % self.uri) - for eid, b64extid in cursor.fetchall(): - extid = b64decode(b64extid) - self.debug('ldap eid %s', eid) - # if no result found, _search automatically delete entity information - res = self._search(session, extid, BASE) - self.debug('ldap search %s', res) - if res: - ldapemailaddr = res[0].get(ldap_emailattr) - if ldapemailaddr: - if isinstance(ldapemailaddr, list): - ldapemailaddr = ldapemailaddr[0] # XXX consider only the first email in the list - rset = execute('Any X,A WHERE ' - 'X address A, U use_email X, U eid %(u)s', - {'u': eid}) - ldapemailaddr = unicode(ldapemailaddr) - for emaileid, emailaddr, in rset: - if emailaddr == ldapemailaddr: - break - else: - self.debug('updating email address of user %s to %s', - extid, ldapemailaddr) - emailrset = execute('EmailAddress A WHERE A address %(addr)s', - {'addr': ldapemailaddr}) - if emailrset: - execute('SET U use_email X WHERE ' - 'X eid %(x)s, U eid %(u)s', - {'x': emailrset[0][0], 'u': eid}) - elif rset: - if not execute('SET X address %(addr)s WHERE ' - 'U primary_email X, U eid %(u)s', - {'addr': ldapemailaddr, 'u': eid}): - execute('SET X address %(addr)s WHERE ' - 'X eid %(x)s', - {'addr': ldapemailaddr, 'x': rset[0][0]}) - else: - # no email found, create it - _insert_email(session, ldapemailaddr, eid) - session.commit() - - def ldap_name(self, var): - if var.stinfo['relations']: - relname = iter(var.stinfo['relations']).next().r_type - return self.user_rev_attrs.get(relname) - return None - - def prepare_columns(self, mainvars, rqlst): - """return two list describing how to build the final results - from the result of an ldap search (ie a list of dictionary) - """ - columns = [] - global_transforms = [] - for i, term in enumerate(rqlst.selection): - if isinstance(term, Constant): - columns.append(term) - continue - if isinstance(term, Function): # LOWER, UPPER, COUNT... - var = term.get_nodes(VariableRef)[0] - var = var.variable - try: - mainvar = var.stinfo['attrvar'].name - except AttributeError: # no attrvar set - mainvar = var.name - assert mainvar in mainvars - trname = term.name - ldapname = self.ldap_name(var) - if trname in ('COUNT', 'MIN', 'MAX', 'SUM'): - global_transforms.append(GlobTrFunc(trname, i, ldapname)) - columns.append((mainvar, ldapname)) - continue - if trname in ('LOWER', 'UPPER'): - columns.append((mainvar, TrFunc(trname, i, ldapname))) - continue - raise NotImplementedError('no support for %s function' % trname) - if term.name in mainvars: - columns.append((term.name, 'dn')) - continue - var = term.variable - mainvar = var.stinfo['attrvar'].name - columns.append((mainvar, self.ldap_name(var))) - #else: - # # probably a bug in rql splitting if we arrive here - # raise NotImplementedError - return columns, global_transforms - - def syntax_tree_search(self, session, union, - args=None, cachekey=None, varmap=None, debug=0): - """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. - """ - self.debug('ldap syntax tree search') - # XXX not handled : transform/aggregat function, join on multiple users... - assert len(union.children) == 1, 'union not supported' - rqlst = union.children[0] - assert not rqlst.with_, 'subquery not supported' - rqlkey = rqlst.as_string(kwargs=args) - try: - results = self._query_cache[rqlkey] - except KeyError: - try: - results = self.rqlst_search(session, rqlst, args) - self._query_cache[rqlkey] = results - 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, txdata=True) - return [] - return results - - def rqlst_search(self, session, rqlst, args): - mainvars = [] - for varname in rqlst.defined_vars: - for sol in rqlst.solutions: - if sol[varname] == 'CWUser': - mainvars.append(varname) - break - assert mainvars, rqlst - columns, globtransforms = self.prepare_columns(mainvars, rqlst) - eidfilters = [lambda x: x > 0] - allresults = [] - generator = RQL2LDAPFilter(self, session, args, mainvars) - for mainvar in mainvars: - # handle restriction - try: - eidfilters_, ldapfilter = generator.generate(rqlst, mainvar) - except GotDN as ex: - assert ex.dn, 'no dn!' - try: - res = [self._cache[ex.dn]] - except KeyError: - res = self._search(session, ex.dn, BASE) - except UnknownEid as ex: - # raised when we are looking for the dn of an eid which is not - # coming from this source - res = [] - else: - eidfilters += eidfilters_ - res = self._search(session, self.user_base_dn, - self.user_base_scope, ldapfilter) - allresults.append(res) - # 1. get eid for each dn and filter according to that eid if necessary - for i, res in enumerate(allresults): - filteredres = [] - for resdict in res: - # get sure the entity exists in the system table - eid = self.repo.extid2eid(self, resdict['dn'], 'CWUser', session) - for eidfilter in eidfilters: - if not eidfilter(eid): - break - else: - resdict['eid'] = eid - filteredres.append(resdict) - allresults[i] = filteredres - # 2. merge result for each "mainvar": cartesian product - allresults = cartesian_product(allresults) - # 3. build final result according to column definition - result = [] - for rawline in allresults: - rawline = dict(zip(mainvars, rawline)) - line = [] - for varname, ldapname in columns: - if ldapname is None: - value = None # no mapping available - elif ldapname == 'dn': - value = rawline[varname]['eid'] - elif isinstance(ldapname, Constant): - if ldapname.type == 'Substitute': - value = args[ldapname.value] - else: - value = ldapname.value - elif isinstance(ldapname, TrFunc): - value = ldapname.apply(rawline[varname]) - else: - value = rawline[varname].get(ldapname) - line.append(value) - result.append(line) - for trfunc in globtransforms: - result = trfunc.apply(result) - #print '--> ldap result', result - return result - - def _process_ldap_item(self, dn, iterator): - itemdict = super(LDAPUserSource, self)._process_ldap_item(dn, iterator) - self._cache[dn] = itemdict - return itemdict - - 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 - entity stored here but the entity has not been inserted in the system - table yet. - - This method must return the an Entity instance representation of this - entity. - """ - self.debug('ldap before entity insertion') - entity = super(LDAPUserSource, self).before_entity_insertion( - session, lid, etype, eid, sourceparams) - res = self._search(session, lid, BASE)[0] - for attr in entity.e_schema.indexable_attributes(): - entity.cw_edited[attr] = res[self.user_rev_attrs[attr]] - return entity - - def after_entity_insertion(self, session, lid, entity, sourceparams): - """called by the repository after an entity stored here has been - inserted in the system table. - """ - self.debug('ldap after entity insertion') - super(LDAPUserSource, self).after_entity_insertion( - session, lid, entity, sourceparams) - 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: - # lid = dn - emailaddr = self._cache[lid][self.user_rev_attrs['email']] - except KeyError: - return - if isinstance(emailaddr, list): - emailaddr = emailaddr[0] # XXX consider only the first email in the list - rset = session.execute('EmailAddress X WHERE X address %(addr)s', - {'addr': emailaddr}) - if rset: - session.execute('SET U primary_email X WHERE U eid %(u)s, X eid %(x)s', - {'x': rset[0][0], 'u': entity.eid}) - else: - # not found, create it - _insert_email(session, emailaddr, entity.eid) - - def update_entity(self, session, entity): - """replace an entity in the source""" - raise RepositoryError('this source is read only') - - def delete_entity(self, session, entity): - """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}) - -class GotDN(Exception): - """exception used when a dn localizing the searched user has been found""" - def __init__(self, dn): - self.dn = dn - - -class RQL2LDAPFilter(object): - """generate an LDAP filter for a rql query""" - def __init__(self, source, session, args=None, mainvars=()): - self.source = source - self.repo = source.repo - self._ldap_attrs = source.user_rev_attrs - self._base_filters = source.base_filters - self._session = session - if args is None: - args = {} - self._args = args - self.mainvars = mainvars - - def generate(self, selection, mainvarname): - self._filters = res = self._base_filters[:] - self._mainvarname = mainvarname - self._eidfilters = [] - self._done_not = set() - restriction = selection.where - if isinstance(restriction, Relation): - # only a single relation, need to append result here (no AND/OR) - filter = restriction.accept(self) - if filter is not None: - res.append(filter) - elif restriction: - restriction.accept(self) - if len(res) > 1: - return self._eidfilters, '(&%s)' % ''.join(res) - return self._eidfilters, res[0] - - def visit_and(self, et): - """generate filter for a AND subtree""" - for c in et.children: - part = c.accept(self) - if part: - self._filters.append(part) - - def visit_or(self, ou): - """generate filter for a OR subtree""" - res = [] - for c in ou.children: - part = c.accept(self) - if part: - res.append(part) - if res: - if len(res) > 1: - part = '(|%s)' % ''.join(res) - else: - part = res[0] - self._filters.append(part) - - def visit_not(self, node): - """generate filter for a OR subtree""" - part = node.children[0].accept(self) - if part: - self._filters.append('(!(%s))'% part) - - def visit_relation(self, relation): - """generate filter for a relation""" - rtype = relation.r_type - # don't care of type constraint statement (i.e. relation_type = 'is') - if rtype == 'is': - return '' - lhs, rhs = relation.get_parts() - # attribute relation - if self.source.schema.rschema(rtype).final: - # dunno what to do here, don't pretend anything else - if lhs.name != self._mainvarname: - if lhs.name in self.mainvars: - # XXX check we don't have variable as rhs - return - raise NotImplementedError - rhs_vars = rhs.get_nodes(VariableRef) - if rhs_vars: - if len(rhs_vars) > 1: - raise NotImplementedError - # selected variable, nothing to do here - return - # no variables in the RHS - if isinstance(rhs.children[0], Function): - res = rhs.children[0].accept(self) - elif rtype != 'has_text': - res = self._visit_attribute_relation(relation) - else: - raise NotImplementedError(relation) - # regular relation XXX todo: in_group - else: - raise NotImplementedError(relation) - return res - - def _visit_attribute_relation(self, relation): - """generate filter for an attribute relation""" - lhs, rhs = relation.get_parts() - lhsvar = lhs.variable - if relation.r_type == 'eid': - # XXX hack - # skip comparison sign - eid = int(rhs.children[0].accept(self)) - if relation.neged(strict=True): - self._done_not.add(relation.parent) - self._eidfilters.append(lambda x: not x == eid) - return - if rhs.operator != '=': - filter = {'>': lambda x: x > eid, - '>=': lambda x: x >= eid, - '<': lambda x: x < eid, - '<=': lambda x: x <= eid, - }[rhs.operator] - self._eidfilters.append(filter) - return - dn = self.repo.eid2extid(self.source, eid, self._session) - raise GotDN(dn) - try: - filter = '(%s%s)' % (self._ldap_attrs[relation.r_type], - rhs.accept(self)) - except KeyError: - # unsupported attribute - self.source.warning('%s source can\'t handle relation %s, no ' - 'results will be returned from this source', - self.source.uri, relation) - raise UnknownEid # trick to return no result - return filter - - def visit_comparison(self, cmp): - """generate filter for a comparaison""" - return '%s%s'% (cmp.operator, cmp.children[0].accept(self)) - - def visit_mathexpression(self, mexpr): - """generate filter for a mathematic expression""" - raise NotImplementedError - - def visit_function(self, function): - """generate filter name for a function""" - if function.name == 'IN': - return self.visit_in(function) - raise NotImplementedError - - def visit_in(self, function): - grandpapa = function.parent.parent - ldapattr = self._ldap_attrs[grandpapa.r_type] - res = [] - for c in function.children: - part = c.accept(self) - if part: - res.append(part) - if res: - if len(res) > 1: - part = '(|%s)' % ''.join('(%s=%s)' % (ldapattr, v) for v in res) - else: - part = '(%s=%s)' % (ldapattr, res[0]) - return part - - def visit_constant(self, constant): - """generate filter name for a constant""" - value = constant.value - if constant.type is None: - raise NotImplementedError - if constant.type == 'Date': - raise NotImplementedError - #value = self.keyword_map[value]() - elif constant.type == 'Substitute': - value = self._args[constant.value] - else: - value = constant.value - if isinstance(value, unicode): - value = value.encode('utf8') - else: - value = str(value) - return escape_filter_chars(value) - - def visit_variableref(self, variableref): - """get the sql name for a variable reference""" - pass - diff -r cf27006ce813 -r b1e933b0e850 server/sqlutils.py --- a/server/sqlutils.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/sqlutils.py Mon Dec 09 16:13:10 2013 +0100 @@ -41,11 +41,6 @@ SQL_PREFIX = 'cw_' def _run_command(cmd): - """backup/restore command are string w/ lgc < 0.47, lists with earlier versions - """ - if isinstance(cmd, basestring): - print '->', cmd - return subprocess.call(cmd, shell=True) print ' '.join(cmd) return subprocess.call(cmd) diff -r cf27006ce813 -r b1e933b0e850 server/test/data/migratedapp/schema.py --- a/server/test/data/migratedapp/schema.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/test/data/migratedapp/schema.py Mon Dec 09 16:13:10 2013 +0100 @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . """cw.server.migraction test""" +import datetime as dt from yams.buildobjs import (EntityType, RelationType, RelationDefinition, SubjectRelation, Bytes, RichString, String, Int, Boolean, Datetime, Date) @@ -62,11 +63,14 @@ 'PE require_permission P, P name "add_note", ' 'P require_group G'),)} - whatever = Int(default=2) # keep it before `date` for unittest_migraction.test_add_attribute_int + whatever = Int(default=0) # keep it before `date` for unittest_migraction.test_add_attribute_int + yesno = Boolean(default=False) date = Datetime() type = String(maxsize=1) unique_id = String(maxsize=1, required=True, unique=True) mydate = Date(default='TODAY') + oldstyledefaultdate = Date(default='2013/01/01') + newstyledefaultdate = Date(default=dt.date(2013, 1, 1)) shortpara = String(maxsize=64, default='hop') ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')]) attachment = SubjectRelation('File') diff -r cf27006ce813 -r b1e933b0e850 server/test/unittest_ldapsource.py --- a/server/test/unittest_ldapsource.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/test/unittest_ldapsource.py Mon Dec 09 16:13:10 2013 +0100 @@ -33,7 +33,6 @@ from cubicweb.devtools.httptest import get_available_port from cubicweb.devtools import get_test_db_handler -from cubicweb.server.sources.ldapuser import GlobTrFunc, UnknownEid, RQL2LDAPFilter CONFIG_LDAPFEED = u''' user-base-dn=ou=People,dc=cubicweb,dc=test @@ -453,386 +452,6 @@ self.setUpClass() -class LDAPUserSourceTC(LDAPFeedTestBase): - test_db_id = 'ldap-user' - tags = CubicWebTC.tags | Tags(('ldap')) - - @classmethod - def pre_setup_database(cls, session, config): - session.create_entity('CWSource', name=u'ldap', type=u'ldapuser', - url=URL, config=CONFIG_LDAPUSER) - session.commit() - # XXX keep it there - session.execute('CWUser U') - - def setup_database(self): - # XXX a traceback may appear in the logs of the test due to - # the _init_repo method that may fail to connect to the ldap - # source if its URI has changed (from what is stored in the - # database). This TB is NOT a failure or so. - with self.session.repo.internal_session(safe=True) as session: - session.execute('SET S url %(url)s, S config %(conf)s ' - 'WHERE S is CWSource, S name "ldap"', - {"conf": CONFIG_LDAPUSER, 'url': URL} ) - session.commit() - self.pull() - - 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['ldap'] - source.synchronize() - - 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, ()) - addresses = [pe.address for pe in e.use_email] - addresses.sort() - # should habe two element but ldapuser seems buggy. It's going to be dropped anyway. - self.assertEqual(['sylvain.thenault@logilab.fr',], # 'syt@logilab.fr'], - addresses) - self.assertIn(e.primary_email[0].address, - ['sylvain.thenault@logilab.fr', 'syt@logilab.fr']) - # 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_not(self): - eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0] - rset = self.sexecute('CWUser X WHERE NOT X eid %s' % eid) - self.assert_(rset) - self.assert_(not eid in (r[0] for r in rset)) - - def test_multiple(self): - seid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0] - aeid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'adim'})[0][0] - rset = self.sexecute('CWUser X, Y WHERE X login %(syt)s, Y login %(adim)s', - {'syt': 'syt', 'adim': 'adim'}) - self.assertEqual(rset.rows, [[seid, aeid]]) - rset = self.sexecute('Any X,Y,L WHERE X login L, X login %(syt)s, Y login %(adim)s', - {'syt': 'syt', 'adim': 'adim'}) - self.assertEqual(rset.rows, [[seid, aeid, 'syt']]) - - def test_in(self): - seid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0] - aeid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'adim'})[0][0] - rset = self.sexecute('Any X,L ORDERBY L WHERE X login IN("%s", "%s"), X login L' % ('syt', 'adim')) - self.assertEqual(rset.rows, [[aeid, 'adim'], [seid, 'syt']]) - - def test_relations(self): - eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0] - rset = self.sexecute('Any X,E WHERE X is CWUser, X login L, X primary_email E') - self.assert_(eid in (r[0] for r in rset)) - rset = self.sexecute('Any X,L,E WHERE X is CWUser, X login L, X primary_email E') - self.assert_('syt' in (r[1] for r in rset)) - - def test_count(self): - nbusers = self.sexecute('Any COUNT(X) WHERE X is CWUser')[0][0] - # just check this is a possible number - self.assert_(nbusers > 1, nbusers) - self.assert_(nbusers < 30, nbusers) - - def test_upper(self): - eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0] - rset = self.sexecute('Any UPPER(L) WHERE X eid %s, X login L' % eid) - self.assertEqual(rset[0][0], 'syt'.upper()) - - def test_unknown_attr(self): - eid = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'})[0][0] - rset = self.sexecute('Any L,C,M WHERE X eid %s, X login L, ' - 'X creation_date C, X modification_date M' % eid) - self.assertEqual(rset[0][0], 'syt') - self.assertEqual(rset[0][1], None) - self.assertEqual(rset[0][2], None) - - def test_sort(self): - logins = [l for l, in self.sexecute('Any L ORDERBY L WHERE X login L')] - self.assertEqual(logins, sorted(logins)) - - def test_lower_sort(self): - logins = [l for l, in self.sexecute('Any L ORDERBY lower(L) WHERE X login L')] - self.assertEqual(logins, sorted(logins)) - - def test_or(self): - rset = self.sexecute('DISTINCT Any X WHERE X login %(login)s OR (X in_group G, G name "managers")', - {'login': 'syt'}) - self.assertEqual(len(rset), 2, rset.rows) # syt + admin - - def test_nonregr_set_owned_by(self): - # test that when a user coming from ldap is triggering a transition - # the related TrInfo has correct owner information - self.sexecute('SET X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': 'syt'}) - self.commit() - syt = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'syt'}).get_entity(0, 0) - self.assertEqual([g.name for g in syt.in_group], ['managers', 'users']) - cnx = self.login('syt', password='syt') - cu = cnx.cursor() - adim = cu.execute('CWUser X WHERE X login %(login)s', {'login': 'adim'}).get_entity(0, 0) - iworkflowable = adim.cw_adapt_to('IWorkflowable') - iworkflowable.fire_transition('deactivate') - try: - cnx.commit() - adim.cw_clear_all_caches() - self.assertEqual(adim.in_state[0].name, 'deactivated') - trinfo = iworkflowable.latest_trinfo() - self.assertEqual(trinfo.owned_by[0].login, 'syt') - # select from_state to skip the user's creation TrInfo - rset = self.sexecute('Any U ORDERBY D DESC WHERE WF wf_info_for X,' - 'WF creation_date D, WF from_state FS,' - 'WF owned_by U?, X eid %(x)s', - {'x': adim.eid}) - self.assertEqual(rset.rows, [[syt.eid]]) - finally: - # restore db state - self.restore_connection() - adim = self.sexecute('CWUser X WHERE X login %(login)s', {'login': 'adim'}).get_entity(0, 0) - adim.cw_adapt_to('IWorkflowable').fire_transition('activate') - self.sexecute('DELETE X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': 'syt'}) - - def test_same_column_names(self): - self.sexecute('Any X, Y WHERE X copain Y, X login "comme", Y login "cochon"') - - def test_multiple_entities_from_different_sources(self): - req = self.request() - self.create_user(req, 'cochon') - self.assertTrue(self.sexecute('Any X,Y WHERE X login %(syt)s, Y login "cochon"', {'syt': 'syt'})) - - def test_exists1(self): - self.session.set_cnxset() - self.session.create_entity('CWGroup', name=u'bougloup1') - self.session.create_entity('CWGroup', name=u'bougloup2') - self.sexecute('SET U in_group G WHERE G name ~= "bougloup%", U login "admin"') - self.sexecute('SET U in_group G WHERE G name = "bougloup1", U login %(syt)s', {'syt': 'syt'}) - rset = self.sexecute('Any L,SN ORDERBY L WHERE X in_state S, ' - 'S name SN, X login L, EXISTS(X in_group G, G name ~= "bougloup%")') - self.assertEqual(rset.rows, [['admin', 'activated'], ['syt', 'activated']]) - - def test_exists2(self): - req = self.request() - self.create_user(req, 'comme') - self.create_user(req, 'cochon') - self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"') - rset = self.sexecute('Any GN ORDERBY GN WHERE X in_group G, G name GN, ' - '(G name "managers" OR EXISTS(X copain T, T login in ("comme", "cochon")))') - self.assertEqual(rset.rows, [['managers'], ['users']]) - - def test_exists3(self): - req = self.request() - self.create_user(req, 'comme') - self.create_user(req, 'cochon') - self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"') - self.assertTrue(self.sexecute('Any X, Y WHERE X copain Y, X login "comme", Y login "cochon"')) - self.sexecute('SET X copain Y WHERE X login %(syt)s, Y login "cochon"', {'syt': 'syt'}) - self.assertTrue(self.sexecute('Any X, Y WHERE X copain Y, X login %(syt)s, Y login "cochon"', {'syt': 'syt'})) - rset = self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, G name "managers" ' - 'OR EXISTS(X copain T, T login in ("comme", "cochon"))') - self.assertEqual(sorted(rset.rows), [['managers', 'admin'], ['users', 'comme'], ['users', 'syt']]) - - def test_exists4(self): - req = self.request() - self.create_user(req, 'comme') - self.create_user(req, 'cochon', groups=('users', 'guests')) - self.create_user(req, 'billy') - self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"') - self.sexecute('SET X copain Y WHERE X login "cochon", Y login "cochon"') - self.sexecute('SET X copain Y WHERE X login "comme", Y login "billy"') - self.sexecute('SET X copain Y WHERE X login %(syt)s, Y login "billy"', {'syt': 'syt'}) - # search for group name, login where - # CWUser copain with "comme" or "cochon" AND same login as the copain - # OR - # CWUser in_state activated AND not copain with billy - # - # SO we expect everybody but "comme" and "syt" - rset= self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, ' - 'EXISTS(X copain T, T login L, T login in ("comme", "cochon")) OR ' - 'EXISTS(X in_state S, S name "activated", NOT X copain T2, T2 login "billy")') - all = self.sexecute('Any GN, L WHERE X in_group G, X login L, G name GN') - all.rows.remove(['users', 'comme']) - all.rows.remove(['users', 'syt']) - self.assertEqual(sorted(rset.rows), sorted(all.rows)) - - def test_exists5(self): - req = self.request() - self.create_user(req, 'comme') - self.create_user(req, 'cochon', groups=('users', 'guests')) - self.create_user(req, 'billy') - self.sexecute('SET X copain Y WHERE X login "comme", Y login "cochon"') - self.sexecute('SET X copain Y WHERE X login "cochon", Y login "cochon"') - self.sexecute('SET X copain Y WHERE X login "comme", Y login "billy"') - self.sexecute('SET X copain Y WHERE X login %(syt)s, Y login "cochon"', {'syt': 'syt'}) - rset= self.sexecute('Any L WHERE X login L, ' - 'EXISTS(X copain T, T login in ("comme", "cochon")) AND ' - 'NOT EXISTS(X copain T2, T2 login "billy")') - self.assertEqual(sorted(rset.rows), [['cochon'], ['syt']]) - rset= self.sexecute('Any GN,L WHERE X in_group G, X login L, G name GN, ' - 'EXISTS(X copain T, T login in ("comme", "cochon")) AND ' - 'NOT EXISTS(X copain T2, T2 login "billy")') - self.assertEqual(sorted(rset.rows), [['guests', 'cochon'], - ['users', 'cochon'], - ['users', 'syt']]) - - def test_cd_restriction(self): - rset = self.sexecute('CWUser X WHERE X creation_date > "2009-02-01"') - # admin/anon but no ldap user since it doesn't support creation_date - self.assertEqual(sorted(e.login for e in rset.entities()), - ['admin', 'anon']) - - def test_union(self): - afeids = self.sexecute('State X') - ueids = self.sexecute('CWUser X') - rset = self.sexecute('(Any X WHERE X is State) UNION (Any X WHERE X is CWUser)') - self.assertEqual(sorted(r[0] for r in rset.rows), - sorted(r[0] for r in afeids + ueids)) - - def _init_security_test(self): - req = self.request() - self.create_user(req, 'iaminguestsgrouponly', groups=('guests',)) - cnx = self.login('iaminguestsgrouponly') - return cnx.cursor() - - def test_security1(self): - cu = self._init_security_test() - rset = cu.execute('CWUser X WHERE X login %(login)s', {'login': 'syt'}) - self.assertEqual(rset.rows, []) - rset = cu.execute('Any X WHERE X login "iaminguestsgrouponly"') - self.assertEqual(len(rset.rows), 1) - - def test_security2(self): - cu = self._init_security_test() - rset = cu.execute('Any X WHERE X has_text %(syt)s', {'syt': 'syt'}) - self.assertEqual(rset.rows, []) - rset = cu.execute('Any X WHERE X has_text "iaminguestsgrouponly"') - self.assertEqual(len(rset.rows), 1) - - def test_security3(self): - cu = self._init_security_test() - rset = cu.execute('Any F WHERE X has_text %(syt)s, X firstname F', {'syt': 'syt'}) - self.assertEqual(rset.rows, []) - rset = cu.execute('Any F WHERE X has_text "iaminguestsgrouponly", X firstname F') - self.assertEqual(rset.rows, [[None]]) - - 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', - {'x': self.session.user.eid}) - - def test_nonregr2(self): - self.sexecute('Any X,L,AA WHERE E eid %(x)s, E owned_by X, ' - 'X login L, X modification_date AA', - {'x': self.session.user.eid}) - - def test_nonregr3(self): - self.sexecute('Any X,AA ORDERBY AA DESC WHERE E eid %(x)s, ' - 'X modification_date AA', - {'x': self.session.user.eid}) - - def test_nonregr4(self): - emaileid = self.sexecute('INSERT EmailAddress X: X address "toto@logilab.org"')[0][0] - self.sexecute('Any X,AA WHERE X use_email Y, Y eid %(x)s, X modification_date AA', - {'x': emaileid}) - - def test_nonregr5(self): - # original jpl query: - # Any X, NOW - CD, P WHERE P is Project, U interested_in P, U is CWUser, - # U login "sthenault", X concerns P, X creation_date CD ORDERBY CD DESC LIMIT 5 - rql = ('Any X, NOW - CD, P ORDERBY CD DESC LIMIT 5 WHERE P bookmarked_by U, ' - 'U login "%s", P is X, X creation_date CD') % self.session.user.login - self.sexecute(rql, )#{'x': }) - - def test_nonregr6(self): - self.sexecute('Any B,U,UL GROUPBY B,U,UL WHERE B created_by U?, B is File ' - 'WITH U,UL BEING (Any U,UL WHERE ME eid %(x)s, (EXISTS(U identity ME) ' - 'OR (EXISTS(U in_group G, G name IN("managers", "staff")))) ' - '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): - trfunc = GlobTrFunc('count', 0) - res = trfunc.apply([[1], [2], [3], [4]]) - self.assertEqual(res, [[4]]) - trfunc = GlobTrFunc('count', 1) - res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]]) - self.assertEqual(res, [[1, 2], [2, 1], [3, 1]]) - - def test_sum(self): - trfunc = GlobTrFunc('sum', 0) - res = trfunc.apply([[1], [2], [3], [4]]) - self.assertEqual(res, [[10]]) - trfunc = GlobTrFunc('sum', 1) - res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]]) - self.assertEqual(res, [[1, 7], [2, 4], [3, 6]]) - - def test_min(self): - trfunc = GlobTrFunc('min', 0) - res = trfunc.apply([[1], [2], [3], [4]]) - self.assertEqual(res, [[1]]) - trfunc = GlobTrFunc('min', 1) - res = trfunc.apply([[1, 2], [2, 4], [3, 6], [1, 5]]) - self.assertEqual(res, [[1, 2], [2, 4], [3, 6]]) - - def test_max(self): - trfunc = GlobTrFunc('max', 0) - res = trfunc.apply([[1], [2], [3], [4]]) - self.assertEqual(res, [[4]]) - trfunc = GlobTrFunc('max', 1) - 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')) - - @property - def schema(self): - """return the application schema""" - return self._schema - - def setUp(self): - self.handler = get_test_db_handler(LDAPUserSourceTC.config) - self.handler.build_db_cache('ldap-user', LDAPUserSourceTC.pre_setup_database) - self.handler.restore_database('ldap-user') - self._repo = repo = self.handler.get_repo() - self._schema = repo.schema - super(RQL2LDAPFilterTC, self).setUp() - ldapsource = repo.sources[-1] - self.cnxset = repo._get_cnxset() - session = mock_object(cnxset=self.cnxset) - self.o = RQL2LDAPFilter(ldapsource, session) - self.ldapclasses = ''.join(ldapsource.base_filters) - - def tearDown(self): - self._repo.turn_repo_off() - super(RQL2LDAPFilterTC, self).tearDown() - - def test_base(self): - rqlst = self._prepare('CWUser X WHERE X login "toto"').children[0] - self.assertEqual(self.o.generate(rqlst, 'X')[1], - '(&%s(uid=toto))' % self.ldapclasses) - - def test_kwargs(self): - rqlst = self._prepare('CWUser X WHERE X login %(x)s').children[0] - self.o._args = {'x': "toto"} - self.assertEqual(self.o.generate(rqlst, 'X')[1], - '(&%s(uid=toto))' % self.ldapclasses) - - def test_get_attr(self): - rqlst = self._prepare('Any X WHERE E firstname X, E eid 12').children[0] - self.assertRaises(UnknownEid, self.o.generate, rqlst, 'E') - if __name__ == '__main__': unittest_main() diff -r cf27006ce813 -r b1e933b0e850 server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/test/unittest_migractions.py Mon Dec 09 16:13:10 2013 +0100 @@ -72,6 +72,22 @@ CubicWebTC.tearDown(self) self.repo.vreg['etypes'].clear_caches() + def test_add_attribute_bool(self): + self.assertFalse('yesno' in self.schema) + self.session.create_entity('Note') + self.commit() + self.mh.cmd_add_attribute('Note', 'yesno') + self.assertTrue('yesno' in self.schema) + self.assertEqual(self.schema['yesno'].subjects(), ('Note',)) + self.assertEqual(self.schema['yesno'].objects(), ('Boolean',)) + self.assertEqual(self.schema['Note'].default('yesno'), False) + # test default value set on existing entities + note = self.session.execute('Note X').get_entity(0, 0) + self.assertEqual(note.yesno, False) + # test default value set for next entities + self.assertEqual(self.session.create_entity('Note').yesno, False) + self.mh.rollback() + def test_add_attribute_int(self): self.assertFalse('whatever' in self.schema) self.session.create_entity('Note') @@ -82,12 +98,13 @@ self.assertTrue('whatever' in self.schema) self.assertEqual(self.schema['whatever'].subjects(), ('Note',)) self.assertEqual(self.schema['whatever'].objects(), ('Int',)) - self.assertEqual(self.schema['Note'].default('whatever'), 2) + self.assertEqual(self.schema['Note'].default('whatever'), 0) # test default value set on existing entities note = self.session.execute('Note X').get_entity(0, 0) - self.assertEqual(note.whatever, 2) + self.assertIsInstance(note.whatever, int) + self.assertEqual(note.whatever, 0) # test default value set for next entities - self.assertEqual(self.session.create_entity('Note').whatever, 2) + self.assertEqual(self.session.create_entity('Note').whatever, 0) # test attribute order orderdict2 = dict(self.mh.rqlexec('Any RTN, O WHERE X name "Note", RDEF from_entity X, ' 'RDEF relation_type RT, RDEF ordernum O, RT name RTN')) @@ -127,9 +144,14 @@ def test_add_datetime_with_default_value_attribute(self): self.assertFalse('mydate' in self.schema) - self.assertFalse('shortpara' in self.schema) + self.assertFalse('oldstyledefaultdate' in self.schema) + self.assertFalse('newstyledefaultdate' in self.schema) self.mh.cmd_add_attribute('Note', 'mydate') + self.mh.cmd_add_attribute('Note', 'oldstyledefaultdate') + self.mh.cmd_add_attribute('Note', 'newstyledefaultdate') self.assertTrue('mydate' in self.schema) + self.assertTrue('oldstyledefaultdate' in self.schema) + self.assertTrue('newstyledefaultdate' in self.schema) self.assertEqual(self.schema['mydate'].subjects(), ('Note', )) self.assertEqual(self.schema['mydate'].objects(), ('Date', )) testdate = date(2005, 12, 13) @@ -137,8 +159,13 @@ eid2 = self.mh.rqlexec('INSERT Note N: N mydate %(mydate)s', {'mydate' : testdate})[0][0] d1 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X mydate D', {'x': eid1})[0][0] d2 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X mydate D', {'x': eid2})[0][0] + d3 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X oldstyledefaultdate D', {'x': eid1})[0][0] + d4 = self.mh.rqlexec('Any D WHERE X eid %(x)s, X newstyledefaultdate D', {'x': eid1})[0][0] self.assertEqual(d1, date.today()) self.assertEqual(d2, testdate) + myfavoritedate = date(2013, 1, 1) + self.assertEqual(d3, myfavoritedate) + self.assertEqual(d4, myfavoritedate) self.mh.rollback() def test_drop_chosen_constraints_ctxmanager(self): diff -r cf27006ce813 -r b1e933b0e850 server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Fri Dec 06 17:20:59 2013 +0100 +++ b/server/test/unittest_schemaserial.py Mon Dec 09 16:13:10 2013 +0100 @@ -22,6 +22,7 @@ from logilab.common.testlib import TestCase, unittest_main +from cubicweb import Binary from cubicweb.schema import CubicWebSchemaLoader from cubicweb.devtools import TestServerConfiguration @@ -188,7 +189,6 @@ self.assertIn('extra_props', got[1][1]) # this extr extra_props = got[1][1]['extra_props'] - from cubicweb import Binary self.assertIsInstance(extra_props, Binary) got[1][1]['extra_props'] = got[1][1]['extra_props'].getvalue() self.assertListEqual(expected, got) @@ -197,7 +197,8 @@ self.assertListEqual([ ('INSERT CWAttribute X: X cardinality %(cardinality)s,X defaultval %(defaultval)s,X description %(description)s,X fulltextindexed %(fulltextindexed)s,X indexed %(indexed)s,X internationalizable %(internationalizable)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE eid %(se)s,ER eid %(rt)s,OE eid %(oe)s', {'se': None, 'rt': None, 'oe': None, - 'description': u'', 'internationalizable': True, 'fulltextindexed': False, 'ordernum': 3, 'defaultval': u'text/plain', 'indexed': False, 'cardinality': u'?1'}), + 'description': u'', 'internationalizable': True, 'fulltextindexed': False, + 'ordernum': 3, 'defaultval': Binary('text/plain'), 'indexed': False, 'cardinality': u'?1'}), ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s', {'x': None, 'value': u'None', 'ct': 'FormatConstraint_eid'}), ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT eid %(ct)s, EDEF eid %(x)s', diff -r cf27006ce813 -r b1e933b0e850 test/data/migration/0.1.0_web.py --- a/test/data/migration/0.1.0_web.py Fri Dec 06 17:20:59 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -# copyright 2003-2010 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 . -"""web only - -""" diff -r cf27006ce813 -r b1e933b0e850 test/data/schema.py --- a/test/data/schema.py Fri Dec 06 17:20:59 2013 +0100 +++ b/test/data/schema.py Mon Dec 09 16:13:10 2013 +0100 @@ -41,7 +41,7 @@ constraints=[RQLConstraint('NOT EXISTS(O contrat_exclusif S)')]) dirige = SubjectRelation('Societe', cardinality='??', constraints=[RQLConstraint('S actionnaire O')]) - associe = SubjectRelation('Personne', cardinality='1*', + associe = SubjectRelation('Personne', cardinality='?*', constraints=[RQLConstraint('S actionnaire SOC, O actionnaire SOC')]) class Ami(EntityType): diff -r cf27006ce813 -r b1e933b0e850 test/unittest_entity.py --- a/test/unittest_entity.py Fri Dec 06 17:20:59 2013 +0100 +++ b/test/unittest_entity.py Mon Dec 09 16:13:10 2013 +0100 @@ -133,6 +133,12 @@ self.assertEqual(sorted(user._cw_related_cache), ['in_group_subject', 'primary_email_subject']) for group in groups: self.assertFalse('in_group_subject' in group._cw_related_cache, list(group._cw_related_cache)) + user.cw_clear_all_caches() + user.related('in_group', entities=True) + self.assertIn('in_group_subject', user._cw_related_cache) + user.cw_clear_all_caches() + user.related('in_group', targettypes=('CWGroup',), entities=True) + self.assertNotIn('in_group_subject', user._cw_related_cache) def test_related_limit(self): req = self.request() @@ -146,6 +152,18 @@ self.assertEqual(len(p.related('tags', 'object', entities=True, limit=2)), 2) self.assertEqual(len(p.related('tags', 'object', entities=True)), 4) + def test_related_targettypes(self): + req = self.request() + p = req.create_entity('Personne', nom=u'Loxodonta', prenom=u'Babar') + n = req.create_entity('Note', type=u'scratch', ecrit_par=p) + t = req.create_entity('Tag', name=u'a tag', tags=(p, n)) + self.commit() + req = self.request() + t = req.entity_from_eid(t.eid) + self.assertEqual(2, t.related('tags').rowcount) + self.assertEqual(1, t.related('tags', targettypes=('Personne',)).rowcount) + self.assertEqual(1, t.related('tags', targettypes=('Note',)).rowcount) + def test_cw_instantiate_relation(self): req = self.request() p1 = req.create_entity('Personne', nom=u'di') @@ -742,7 +760,7 @@ self.assertEqual(card.absolute_url(), 'http://testing.fr/cubicweb/%s' % card.eid) - def test_create_entity(self): + def test_create_and_compare_entity(self): req = self.request() p1 = req.create_entity('Personne', nom=u'fayolle', prenom=u'alexandre') p2 = req.create_entity('Personne', nom=u'campeas', prenom=u'aurelien') @@ -756,6 +774,15 @@ self.assertEqual(sorted([c.nom for c in p.evaluee]), ['campeas', 'fayolle']) self.assertEqual([c.type for c in p.reverse_ecrit_par], ['z']) + req = self.request() + auc = req.execute('Personne P WHERE P prenom "aurelien"').get_entity(0,0) + persons = set() + persons.add(p1) + persons.add(p2) + persons.add(auc) + self.assertEqual(2, len(persons)) + self.assertNotEqual(p1, p2) + self.assertEqual(p2, auc) if __name__ == '__main__': diff -r cf27006ce813 -r b1e933b0e850 test/unittest_mail.py --- a/test/unittest_mail.py Fri Dec 06 17:20:59 2013 +0100 +++ b/test/unittest_mail.py Mon Dec 09 16:13:10 2013 +0100 @@ -31,7 +31,7 @@ def getlogin(): - """avoid usinng os.getlogin() because of strange tty / stdin problems + """avoid using os.getlogin() because of strange tty / stdin problems (man 3 getlogin) Another solution would be to use $LOGNAME, $USER or $USERNAME """ diff -r cf27006ce813 -r b1e933b0e850 test/unittest_migration.py --- a/test/unittest_migration.py Fri Dec 06 17:20:59 2013 +0100 +++ b/test/unittest_migration.py Mon Dec 09 16:13:10 2013 +0100 @@ -79,10 +79,6 @@ self.assert_(not isinstance(config.migration_handler(), ServerMigrationHelper)) self.assertIsInstance(config.migration_handler(), MigrationHelper) config = self.config - config.__class__.name = 'twisted' - self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), - [((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')]) config.__class__.name = 'repository' self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'), @@ -92,8 +88,7 @@ self.assertListEqual(filter_scripts(config, TMIGRDIR, (0,0,4), (0,1,0)), [((0, 1 ,0), TMIGRDIR+'0.1.0_Any.py'), ((0, 1 ,0), TMIGRDIR+'0.1.0_common.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py'), - ((0, 1 ,0), TMIGRDIR+'0.1.0_web.py')]) + ((0, 1 ,0), TMIGRDIR+'0.1.0_repository.py')]) config.__class__.name = 'repository' diff -r cf27006ce813 -r b1e933b0e850 view.py --- a/view.py Fri Dec 06 17:20:59 2013 +0100 +++ b/view.py Mon Dec 09 16:13:10 2013 +0100 @@ -558,34 +558,6 @@ __registry__ = 'adapters' -def implements_adapter_compat(iface): - def _pre39_compat(func): - def decorated(self, *args, **kwargs): - entity = self.entity - if hasattr(entity, func.__name__): - warn('[3.9] %s method is deprecated, define it on a custom ' - '%s for %s instead' % (func.__name__, iface, - entity.__class__), - DeprecationWarning) - member = getattr(entity, func.__name__) - if callable(member): - return member(*args, **kwargs) - return member - return func(self, *args, **kwargs) - decorated.decorated = func - return decorated - return _pre39_compat - - -def unwrap_adapter_compat(cls): - parent = cls.__bases__[0] - for member_name in dir(parent): - member = getattr(parent, member_name) - if isinstance(member, types.MethodType) and hasattr(member.im_func, 'decorated') and not member_name in cls.__dict__: - method = new.instancemethod(member.im_func.decorated, None, cls) - setattr(cls, member_name, method) - - class auto_unwrap_bw_compat(type): def __new__(mcs, name, bases, classdict): cls = type.__new__(mcs, name, bases, classdict) @@ -596,7 +568,6 @@ class EntityAdapter(Adapter): """base class for entity adapters (eg adapt an entity to an interface)""" - __metaclass__ = auto_unwrap_bw_compat def __init__(self, _cw, **kwargs): try: self.entity = kwargs.pop('entity') diff -r cf27006ce813 -r b1e933b0e850 web/__init__.py --- a/web/__init__.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/__init__.py Mon Dec 09 16:13:10 2013 +0100 @@ -31,8 +31,6 @@ from cubicweb.uilib import eid_param assert json_dumps is not None, 'no json module installed' -dumps = deprecated('[3.9] use cubicweb.utils.json_dumps instead of dumps')( - json_dumps) INTERNAL_FIELD_VALUE = '__cubicweb_internal_field__' diff -r cf27006ce813 -r b1e933b0e850 web/data/cubicweb.ajax.js --- a/web/data/cubicweb.ajax.js Fri Dec 06 17:20:59 2013 +0100 +++ b/web/data/cubicweb.ajax.js Mon Dec 09 16:13:10 2013 +0100 @@ -339,11 +339,6 @@ cw.log('loadxhtml called without an element'); } var callback = null; - if (form && form.callback) { - cw.log('[3.9] callback given through form.callback is deprecated, add ' + 'callback on the defered'); - callback = form.callback; - delete form.callback; - } var node = this.get(0); // only consider the first element if (cursor) { setProgressCursor(); @@ -734,13 +729,7 @@ /* DEPRECATED *****************************************************************/ -preprocessAjaxLoad = cw.utils.deprecatedFunction( - '[3.9] preprocessAjaxLoad() is deprecated, use loadAjaxHtmlHead instead', - function(node, newdomnode) { - return loadAjaxHtmlHead(newdomnode); - } -); - +// still used in cwo and keyword cubes at least reloadComponent = cw.utils.deprecatedFunction( '[3.9] reloadComponent() is deprecated, use loadxhtml instead', function(compid, rql, registry, nodeid, extraargs) { @@ -754,52 +743,6 @@ } ); -reloadBox = cw.utils.deprecatedFunction( - '[3.9] reloadBox() is deprecated, use loadxhtml instead', - function(boxid, rql) { - return reloadComponent(boxid, rql, 'ctxcomponents', boxid); - } -); - -replacePageChunk = cw.utils.deprecatedFunction( - '[3.9] replacePageChunk() is deprecated, use loadxhtml instead', - function(nodeId, rql, vid, extraparams, /* ... */ swap, callback) { - var params = null; - if (callback) { - params = { - callback: callback - }; - } - var node = jQuery('#' + nodeId)[0]; - var props = {}; - if (node) { - props['rql'] = rql; - props['fname'] = 'view'; - props['pageid'] = pageid; - if (vid) { - props['vid'] = vid; - } - if (extraparams) { - jQuery.extend(props, extraparams); - } - // FIXME we need to do asURL(props) manually instead of - // passing `props` directly to loadxml because replacePageChunk - // is sometimes called (abusively) with some extra parameters in `vid` - var mode = swap ? 'swap': 'replace'; - var url = AJAX_BASE_URL + asURL(props); - jQuery(node).loadxhtml(url, params, 'get', mode); - } else { - cw.log('Node', nodeId, 'not found'); - } - } -); - -loadxhtml = cw.utils.deprecatedFunction( - '[3.9] loadxhtml() function is deprecated, use loadxhtml method instead', - function(nodeid, url, /* ... */ replacemode) { - jQuery('#' + nodeid).loadxhtml(url, null, 'post', replacemode); - } -); function remoteExec(fname /* ... */) { setProgressCursor(); diff -r cf27006ce813 -r b1e933b0e850 web/data/cubicweb.compat.js --- a/web/data/cubicweb.compat.js Fri Dec 06 17:20:59 2013 +0100 +++ b/web/data/cubicweb.compat.js Mon Dec 09 16:13:10 2013 +0100 @@ -1,34 +1,3 @@ -cw.utils.movedToNamespace(['log', 'jqNode', 'getNode', 'evalJSON', 'urlEncode', - 'swapDOM'], cw); -cw.utils.movedToNamespace(['nodeWalkDepthFirst', 'formContents', 'isArray', - 'isString', 'isArrayLike', 'sliceList', - 'toISOTimestamp'], cw.utils); - - -if ($.noop === undefined) { - function noop() {} -} else { - noop = cw.utils.deprecatedFunction( - '[3.9] noop() is deprecated, use $.noop() instead (XXX requires jQuery 1.4)', - $.noop); -} - -// ========== ARRAY EXTENSIONS ========== /// -Array.prototype.contains = cw.utils.deprecatedFunction( - '[3.9] array.contains(elt) is deprecated, use $.inArray(elt, array)!=-1 instead', - function(element) { - return jQuery.inArray(element, this) != - 1; - } -); - -// ========== END OF ARRAY EXTENSIONS ========== /// -forEach = cw.utils.deprecatedFunction( - '[3.9] forEach() is deprecated, use $.each() instead', - function(array, func) { - return $.each(array, func); - } -); - /** * .. function:: cw.utils.deprecatedFunction(msg, function) * @@ -41,64 +10,20 @@ * [ ["a", "b", "c"], ["d", "e"] ] */ // XXX why not the same argument order as $.map and forEach ? -map = cw.utils.deprecatedFunction( - '[3.9] map() is deprecated, use $.map instead', - function(func, array) { - var result = []; - for (var i = 0, length = array.length; i < length; i++) { - result.push(func(array[i])); - } - return result; - } -); -findValue = cw.utils.deprecatedFunction( - '[3.9] findValue(array, elt) is deprecated, use $.inArray(elt, array) instead', - function(array, element) { - return jQuery.inArray(element, array); - } -); - -filter = cw.utils.deprecatedFunction( - '[3.9] filter(func, array) is deprecated, use $.grep(array, f) instead', - function(func, array) { - return $.grep(array, func); +function map(func, array) { + var result = []; + for (var i = 0, length = array.length; i < length; i++) { + result.push(func(array[i])); } -); - -addElementClass = cw.utils.deprecatedFunction( - '[3.9] addElementClass(node, cls) is deprecated, use $(node).addClass(cls) instead', - function(node, klass) { - $(node).addClass(klass); - } -); + return result; +} -removeElementClass = cw.utils.deprecatedFunction( - '[3.9] removeElementClass(node, cls) is deprecated, use $(node).removeClass(cls) instead', - function(node, klass) { - $(node).removeClass(klass); - } -); -hasElementClass = cw.utils.deprecatedFunction( - '[3.9] hasElementClass(node, cls) is deprecated, use $(node).hasClass(cls)', - function(node, klass) { - return $(node).hasClass(klass); - } -); - +// skm cube still uses this getNodeAttribute = cw.utils.deprecatedFunction( '[3.9] getNodeAttribute(node, attr) is deprecated, use $(node).attr(attr)', function(node, attribute) { return $(node).attr(attribute); } ); - -/** - * The only known usage of KEYS is in the tag cube. Once cubicweb-tag 1.7.0 is out, - * this current definition can be removed. - */ -var KEYS = { - KEY_ESC: 27, - KEY_ENTER: 13 -}; diff -r cf27006ce813 -r b1e933b0e850 web/data/cubicweb.css --- a/web/data/cubicweb.css Fri Dec 06 17:20:59 2013 +0100 +++ b/web/data/cubicweb.css Mon Dec 09 16:13:10 2013 +0100 @@ -17,9 +17,6 @@ } h1, h2, h3 { margin-top:0; margin-bottom:0; } -/* got rhythm ? beat of 12*1.25 = 15 px */ -.rhythm_bg { background: url("%(baseRhythmBg)s") repeat ! important; } - h1, .vtitle { font-size: %(h1FontSize)s; @@ -297,7 +294,6 @@ div#pageContent { clear: both; - /* margin-top:-1px; *//* enable when testing rhythm */ background: %(pageContentBgColor)s; border: 1px solid %(pageContentBorderColor)s; padding: 0 %(pageContentPadding)s %(pageContentPadding)s; diff -r cf27006ce813 -r b1e933b0e850 web/data/cubicweb.htmlhelpers.js --- a/web/data/cubicweb.htmlhelpers.js Fri Dec 06 17:20:59 2013 +0100 +++ b/web/data/cubicweb.htmlhelpers.js Mon Dec 09 16:13:10 2013 +0100 @@ -78,10 +78,10 @@ // generate a list of couple key=value if key is multivalued if (cw.utils.isArrayLike(value)) { for (var i = 0; i < value.length; i++) { - chunks.push(key + '=' + urlEncode(value[i])); + chunks.push(key + '=' + cw.urlEncode(value[i])); } } else { - chunks.push(key + '=' + urlEncode(value)); + chunks.push(key + '=' + cw.urlEncode(value)); } } return chunks.join('&'); diff -r cf27006ce813 -r b1e933b0e850 web/data/cubicweb.js --- a/web/data/cubicweb.js Fri Dec 06 17:20:59 2013 +0100 +++ b/web/data/cubicweb.js Mon Dec 09 16:13:10 2013 +0100 @@ -45,6 +45,15 @@ return null; }, + // escapes string selectors (e.g. "foo.[subject]:42" -> "foo\.\[subject\]\:42" + escape: function(selector) { + if (typeof(selector) == 'string') { + return selector.replace( /(:|\.|\[|\])/g, "\\$1" ); + } + // cw.log('non string selector', selector); + return ''; + }, + getNode: function (node) { if (typeof(node) == 'string') { return document.getElementById(node); @@ -105,15 +114,6 @@ }; }, - movedToNamespace: function (funcnames, namespace) { - for (var i = 0; i < funcnames.length; i++) { - var funcname = funcnames[i]; - var msg = ('[3.9] ' + funcname + ' is deprecated, use ' + - namespace.__name__ + '.' + funcname + ' instead'); - window[funcname] = cw.utils.deprecatedFunction(msg, namespace[funcname]); - } - }, - createDomFunction: function (tag) { function builddom(params, children) { var node = document.createElement(tag); @@ -388,14 +388,6 @@ }); -String.prototype.startsWith = cw.utils.deprecatedFunction('[3.9] str.startsWith() is deprecated, use str.startswith() instead', function (prefix) { - return this.startswith(prefix); -}); - -String.prototype.endsWith = cw.utils.deprecatedFunction('[3.9] str.endsWith() is deprecated, use str.endswith() instead', function (suffix) { - return this.endswith(prefix); -}); - /** DOM factories ************************************************************/ A = cw.utils.createDomFunction('a'); BUTTON = cw.utils.createDomFunction('button'); @@ -472,7 +464,8 @@ return node; } -// XXX avoid crashes / backward compat +// cubes: tag, keyword and apycot seem to use this, including require/provide +// backward compat CubicWeb = cw; jQuery.extend(cw, { diff -r cf27006ce813 -r b1e933b0e850 web/data/cubicweb.rhythm.js --- a/web/data/cubicweb.rhythm.js Fri Dec 06 17:20:59 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -$(document).ready(function() { - $('a.rhythm').click(function (event){ - $('div#pageContent').toggleClass('rhythm_bg'); - $('div#page').toggleClass('rhythm_bg'); - event.preventDefault(); - }); -}); diff -r cf27006ce813 -r b1e933b0e850 web/data/rhythm15.png Binary file web/data/rhythm15.png has changed diff -r cf27006ce813 -r b1e933b0e850 web/data/rhythm18.png Binary file web/data/rhythm18.png has changed diff -r cf27006ce813 -r b1e933b0e850 web/data/rhythm20.png Binary file web/data/rhythm20.png has changed diff -r cf27006ce813 -r b1e933b0e850 web/data/rhythm22.png Binary file web/data/rhythm22.png has changed diff -r cf27006ce813 -r b1e933b0e850 web/data/rhythm24.png Binary file web/data/rhythm24.png has changed diff -r cf27006ce813 -r b1e933b0e850 web/data/rhythm26.png Binary file web/data/rhythm26.png has changed diff -r cf27006ce813 -r b1e933b0e850 web/data/uiprops.py --- a/web/data/uiprops.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/data/uiprops.py Mon Dec 09 16:13:10 2013 +0100 @@ -66,7 +66,6 @@ defaultSize = '12px' defaultLineHeight = '1.5' defaultLineHeightEm = lazystr('%(defaultLineHeight)sem') -baseRhythmBg = 'rhythm18.png' inputHeight = '1.3em' inputPadding = 'O.2em' diff -r cf27006ce813 -r b1e933b0e850 web/formfields.py --- a/web/formfields.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/formfields.py Mon Dec 09 16:13:10 2013 +0100 @@ -404,17 +404,6 @@ for label, value in vocab] if self.sort: vocab = vocab_sort(vocab) - # XXX pre 3.9 bw compat - for i, option in enumerate(vocab): - # option may be a 2 or 3-uple (see Select widget _render method for - # explanation) - value = option[1] - if value is not None and not isinstance(value, basestring): - warn('[3.9] %s: vocabulary value should be an unicode string' - % self, DeprecationWarning) - option = list(option) - option[1] = unicode(value) - vocab[i] = option return vocab # support field as argument to avoid warning when used as format field value @@ -760,8 +749,13 @@ # raise UnmodifiedField instead of returning None, since the later # will try to remove already attached file if any raise UnmodifiedField() - # value is a 2-uple (filename, stream) + # value is a 2-uple (filename, stream) or a list of such + # tuples (multiple files) try: + if isinstance(value, list): + value = value[0] + form.warning('mutiple files provided, however ' + 'only the first will be picked') filename, stream = value except ValueError: raise UnmodifiedField() diff -r cf27006ce813 -r b1e933b0e850 web/formwidgets.py --- a/web/formwidgets.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/formwidgets.py Mon Dec 09 16:13:10 2013 +0100 @@ -588,13 +588,7 @@ def _render(self, form, field, renderer): curvalues, attrs = self.values_and_attributes(form, field) domid = attrs.pop('id', None) - # XXX turn this as initializer argument - try: - sep = attrs.pop('separator') - warn('[3.8] separator should be specified using initializer argument', - DeprecationWarning) - except KeyError: - sep = self.separator + sep = self.separator options = [] for i, option in enumerate(field.vocabulary(form)): try: diff -r cf27006ce813 -r b1e933b0e850 web/request.py --- a/web/request.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/request.py Mon Dec 09 16:13:10 2013 +0100 @@ -684,12 +684,6 @@ cssfile = self.data_url(cssfile) add_css(cssfile, media, *extraargs) - @deprecated('[3.9] use ajax_replace_url() instead, naming rql and vid arguments') - def build_ajax_replace_url(self, nodeid, rql, vid, replacemode='replace', - **extraparams): - return self.ajax_replace_url(nodeid, replacemode, rql=rql, vid=vid, - **extraparams) - def ajax_replace_url(self, nodeid, replacemode='replace', **extraparams): """builds an ajax url that will replace nodeid's content @@ -992,20 +986,6 @@ def html_content_type(self): return 'text/html' - @deprecated('[3.9] use req.uiprops[rid]') - def external_resource(self, rid, default=_MARKER): - """return a path to an external resource, using its identifier - - raise `KeyError` if the resource is not defined - """ - try: - return self.uiprops[rid] - except KeyError: - if default is _MARKER: - raise - return default - - ## HTTP-accept parsers / utilies ############################################## def _mimetype_sort_key(accept_info): diff -r cf27006ce813 -r b1e933b0e850 web/test/data/views.py --- a/web/test/data/views.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/test/data/views.py Mon Dec 09 16:13:10 2013 +0100 @@ -18,6 +18,7 @@ from cubicweb.web import Redirect from cubicweb.web.application import CubicWebPublisher +from cubicweb.web.views.ajaxcontroller import ajaxfunc # 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 @@ -40,5 +41,35 @@ assert req.user.login == login return orig_handle(self, req, path) + +def _recursive_replace_stream_by_content(tree): + """ Search for streams (i.e. object that have a 'read' method) in a tree + (which branches are lists or tuples), and substitute them by their content, + leaving other leafs identical. A copy of the tree with only lists as + branches is returned. + """ + if not isinstance(tree, (list, tuple)): + if hasattr(tree, 'read'): + return tree.read() + return tree + else: + return [_recursive_replace_stream_by_content(value) + for value in tree] + + +@ajaxfunc(output_type='json') +def fileupload(self): + """ Return a json copy of the web request formin which uploaded files + are read and their content substitute the received streams. + """ + try: + result_dict = {} + for key, value in self._cw.form.iteritems(): + result_dict[key] = _recursive_replace_stream_by_content(value) + return result_dict + except Exception, ex: + import traceback as tb + tb.print_exc(ex) + orig_handle = CubicWebPublisher.main_handle_request CubicWebPublisher.main_handle_request = auto_login_handle_request diff -r cf27006ce813 -r b1e933b0e850 web/test/jstests/test_utils.js --- a/web/test/jstests/test_utils.js Fri Dec 06 17:20:59 2013 +0100 +++ b/web/test/jstests/test_utils.js Mon Dec 09 16:13:10 2013 +0100 @@ -47,11 +47,11 @@ module("sliceList"); test("test slicelist", function() { var list = ['a', 'b', 'c', 'd', 'e', 'f']; - same(sliceList(list, 2), ['c', 'd', 'e', 'f']); - same(sliceList(list, 2, -2), ['c', 'd']); - same(sliceList(list, -3), ['d', 'e', 'f']); - same(sliceList(list, 0, -2), ['a', 'b', 'c', 'd']); - same(sliceList(list), list); + same(cw.utils.sliceList(list, 2), ['c', 'd', 'e', 'f']); + same(cw.utils.sliceList(list, 2, -2), ['c', 'd']); + same(cw.utils.sliceList(list, -3), ['d', 'e', 'f']); + same(cw.utils.sliceList(list, 0, -2), ['a', 'b', 'c', 'd']); + same(cw.utils.sliceList(list), list); }); module("formContents", { @@ -83,7 +83,7 @@ 'value="one" />'); $('#test-form').append(''); - same(formContents($('#test-form')[0]), [ + same(cw.utils.formContents($('#test-form')[0]), [ ['input-text', 'mytextarea', 'choice', 'check', 'theselect'], ['toto', 'Hello World!', 'no', 'no', 'foo'] ]); diff -r cf27006ce813 -r b1e933b0e850 web/test/unittest_web.py --- a/web/test/unittest_web.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/test/unittest_web.py Mon Dec 09 16:13:10 2013 +0100 @@ -16,7 +16,17 @@ # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . +from json import loads +from os.path import join + +try: + import requests + assert [int(n) for n in requests.__version__.split('.', 2)][:2] >= [1, 2] +except (ImportError, AssertionError): + requests = None + from logilab.common.testlib import TestCase, unittest_main +from cubicweb.devtools.httptest import CubicWebServerTC from cubicweb.devtools.fake import FakeRequest class AjaxReplaceUrlTC(TestCase): @@ -43,5 +53,45 @@ (cbname, qs, req.pageid), req.html_headers.post_inlined_scripts[0]) + +class FileUploadTC(CubicWebServerTC): + + def setUp(self): + "Skip whole test class if a suitable requests module is not available" + if requests is None: + self.skipTest('Python ``requests`` module is not available') + super(FileUploadTC, self).setUp() + + @property + def _post_url(self): + return self.request().build_url('ajax', fname='fileupload') + + def _fobject(self, fname): + return open(join(self.datadir, fname), 'rb') + + def _fcontent(self, fname): + return self._fobject(fname).read() + + def test_single_file_upload(self): + files = {'file': ('schema.py', self._fobject('schema.py'))} + webreq = requests.post(self._post_url, files=files) + # check backward compat : a single uploaded file leads to a single + # 2-uple in the request form + expect = {'fname': u'fileupload', + 'file': ['schema.py', self._fcontent('schema.py')]} + self.assertEqual(webreq.status_code, 200) + self.assertDictEqual(expect, loads(webreq.content)) + + def test_multiple_file_upload(self): + files = [('files', ('schema.py', self._fobject('schema.py'))), + ('files', ('views.py', self._fobject('views.py')))] + webreq = requests.post(self._post_url, files=files,) + expect = {'fname': u'fileupload', + 'files': [['schema.py', self._fcontent('schema.py')], + ['views.py', self._fcontent('views.py')]],} + self.assertEqual(webreq.status_code, 200) + self.assertDictEqual(expect, loads(webreq.content)) + + if __name__ == '__main__': unittest_main() diff -r cf27006ce813 -r b1e933b0e850 web/views/actions.py --- a/web/views/actions.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/actions.py Mon Dec 09 16:13:10 2013 +0100 @@ -419,22 +419,6 @@ def url(self): return 'http://www.cubicweb.org' -class GotRhythmAction(action.Action): - __regid__ = 'rhythm' - __select__ = debug_mode() - - category = 'footer' - order = 3 - title = _('Got rhythm?') - - def url(self): - return xml_escape(self._cw.url()+'#') - - def html_class(self): - self._cw.add_js('cubicweb.rhythm.js') - return 'rhythm' - - ## default actions ui configuration ########################################### addmenu = uicfg.actionbox_appearsin_addmenu diff -r cf27006ce813 -r b1e933b0e850 web/views/autoform.py --- a/web/views/autoform.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/autoform.py Mon Dec 09 16:13:10 2013 +0100 @@ -726,16 +726,17 @@ # action on the form tag _default_form_action_path = 'validateform' - # pre 3.8.3 compat + @deprecated('[3.18] you should override form_action()') def set_action(self, action): self._action = action + + @deprecated('[3.18] use form_action()') def get_action(self): try: return self._action except AttributeError: return self._cw.build_url(self._default_form_action_path) - action = property(deprecated('[3.9] use form.form_action()')(get_action), - set_action) + @iclassmethod def field_by_name(cls_or_self, name, role=None, eschema=None): diff -r cf27006ce813 -r b1e933b0e850 web/views/baseviews.py --- a/web/views/baseviews.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/baseviews.py Mon Dec 09 16:13:10 2013 +0100 @@ -424,11 +424,7 @@ redirect_vid = 'incontext' def call(self, subvid=None, **kwargs): - if subvid is None and 'vid' in kwargs: - warn("[3.9] should give a 'subvid' argument instead of 'vid'", - DeprecationWarning, stacklevel=2) - else: - kwargs['vid'] = subvid + kwargs['vid'] = subvid rset = self.cw_rset for i in xrange(len(rset)): self.cell_call(i, 0, **kwargs) @@ -643,14 +639,3 @@ RssView = class_moved(xmlrss.RSSView) RssItemView = class_moved(xmlrss.RSSItemView) TableView = class_moved(tableview.TableView) - - -class SecondaryView(EntityView): - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] the secondary view is deprecated, use one of oneline/incontext/outofcontext' - __regid__ = 'secondary' - - def cell_call(self, row, col, **kwargs): - entity = self.cw_rset.get_entity(row, col) - self.w(u' ') - self.wview('oneline', self.cw_rset, row=row, col=col) diff -r cf27006ce813 -r b1e933b0e850 web/views/calendar.py --- a/web/views/calendar.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/calendar.py Mon Dec 09 16:13:10 2013 +0100 @@ -27,9 +27,8 @@ from logilab.common.date import todatetime from cubicweb.utils import json_dumps, make_uid -from cubicweb.interfaces import ICalendarable -from cubicweb.predicates import implements, adaptable -from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat +from cubicweb.predicates import adaptable +from cubicweb.view import EntityView, EntityAdapter # useful constants & functions ################################################ @@ -46,16 +45,14 @@ class ICalendarableAdapter(EntityAdapter): __needs_bw_compat__ = True __regid__ = 'ICalendarable' - __select__ = implements(ICalendarable, warn=False) # XXX for bw compat, should be abstract + __abstract__ = True @property - @implements_adapter_compat('ICalendarable') def start(self): """return start date""" raise NotImplementedError @property - @implements_adapter_compat('ICalendarable') def stop(self): """return stop date""" raise NotImplementedError diff -r cf27006ce813 -r b1e933b0e850 web/views/cwuser.py --- a/web/views/cwuser.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/cwuser.py Mon Dec 09 16:13:10 2013 +0100 @@ -189,8 +189,8 @@ __select__ = StartupView.__select__ & match_user_groups('managers') cache_max_age = 0 # disable caching # XXX one could wish to display for instance only user's firstname/surname - # for non managers but filtering out NULL cause crash with an ldapuser - # source. + # for non managers but filtering out NULL caused crash with an ldapuser + # source. The ldapuser source has been dropped and this code can be updated. rql = ('Any U,US,F,S,U,UAA,UDS, L,UAA,USN,UDSN ORDERBY L WHERE U is CWUser, ' 'U login L, U firstname F, U surname S, ' 'U in_state US, US name USN, ' diff -r cf27006ce813 -r b1e933b0e850 web/views/editcontroller.py --- a/web/views/editcontroller.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/editcontroller.py Mon Dec 09 16:13:10 2013 +0100 @@ -28,7 +28,7 @@ from rql.utils import rqlvar_maker from cubicweb import Binary, ValidationError -from cubicweb.view import EntityAdapter, implements_adapter_compat +from cubicweb.view import EntityAdapter from cubicweb.predicates import is_instance from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError) @@ -36,7 +36,6 @@ class IEditControlAdapter(EntityAdapter): - __needs_bw_compat__ = True __regid__ = 'IEditControl' __select__ = is_instance('Any') @@ -47,7 +46,6 @@ DeprecationWarning) super(IEditControlAdapter, self).__init__(_cw, **kwargs) - @implements_adapter_compat('IEditControl') def after_deletion_path(self): """return (path, parameters) which should be used as redirect information when this entity is being deleted @@ -57,7 +55,6 @@ return parent.rest_path(), {} return str(self.entity.e_schema).lower(), {} - @implements_adapter_compat('IEditControl') def pre_web_edit(self): """callback called by the web editcontroller when an entity will be created/modified, to let a chance to do some entity specific stuff. diff -r cf27006ce813 -r b1e933b0e850 web/views/ibreadcrumbs.py --- a/web/views/ibreadcrumbs.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/ibreadcrumbs.py Mon Dec 09 16:13:10 2013 +0100 @@ -24,7 +24,6 @@ from logilab.mtconverter import xml_escape -#from cubicweb.interfaces import IBreadCrumbs from cubicweb import tags, uilib from cubicweb.entity import Entity from cubicweb.predicates import (is_instance, one_line_rset, adaptable, @@ -35,15 +34,6 @@ # don't use AnyEntity since this may cause bug with isinstance() due to reloading -# ease bw compat -def ibreadcrumb_adapter(entity): - if hasattr(entity, 'breadcrumbs'): - warn('[3.9] breadcrumbs() method is deprecated, define a custom ' - 'IBreadCrumbsAdapter for %s instead' % entity.__class__, - DeprecationWarning) - return entity - return entity.cw_adapt_to('IBreadCrumbs') - class IBreadCrumbsAdapter(EntityAdapter): """adapters for entities which can be"located" on some path to display in @@ -53,11 +43,6 @@ __select__ = is_instance('Any', accept_none=False) def parent_entity(self): - if hasattr(self.entity, 'parent') and callable(self.entity.parent): - warn('[3.9] parent() method is deprecated, define a ' - 'custom IBreadCrumbsAdapter/ITreeAdapter for %s instead' - % self.entity.__class__, DeprecationWarning) - return self.entity.parent() itree = self.entity.cw_adapt_to('ITree') if itree is not None: return itree.parent() @@ -94,7 +79,7 @@ self.error('cycle in breadcrumbs for entity %s' % self.entity) return [] _recurs.add(parent.eid) - adapter = ibreadcrumb_adapter(parent) + adapter = parent.cw_adapt_to('IBreadCrumbs') path = adapter.breadcrumbs(view, _recurs) + [self.entity] else: path = [self.entity] @@ -125,7 +110,7 @@ entity = self.cw_extra_kwargs['entity'] except KeyError: entity = self.cw_rset.get_entity(0, 0) - adapter = ibreadcrumb_adapter(entity) + adapter = entity.cw_adapt_to('IBreadCrumbs') view = self.cw_extra_kwargs.get('view') path = adapter.breadcrumbs(view) if path: diff -r cf27006ce813 -r b1e933b0e850 web/views/navigation.py --- a/web/views/navigation.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/navigation.py Mon Dec 09 16:13:10 2013 +0100 @@ -55,10 +55,9 @@ from logilab.mtconverter import xml_escape from logilab.common.deprecation import deprecated -from cubicweb.predicates import (paginated_rset, sorted_rset, - adaptable, implements) +from cubicweb.predicates import paginated_rset, sorted_rset, adaptable from cubicweb.uilib import cut -from cubicweb.view import EntityAdapter, implements_adapter_compat +from cubicweb.view import EntityAdapter from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent @@ -324,7 +323,6 @@ View.handle_pagination = False -from cubicweb.interfaces import IPrevNext class IPrevNextAdapter(EntityAdapter): """Interface for entities which can be linked to a previous and/or next @@ -335,14 +333,12 @@ """ __needs_bw_compat__ = True __regid__ = 'IPrevNext' - __select__ = implements(IPrevNext, warn=False) # XXX for bw compat, else should be abstract + __abstract__ = True - @implements_adapter_compat('IPrevNext') def next_entity(self): """return the 'next' entity""" raise NotImplementedError - @implements_adapter_compat('IPrevNext') def previous_entity(self): """return the 'previous' entity""" raise NotImplementedError diff -r cf27006ce813 -r b1e933b0e850 web/views/primary.py --- a/web/views/primary.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/primary.py Mon Dec 09 16:13:10 2013 +0100 @@ -189,10 +189,6 @@ def render_entity_toolbox(self, entity): self.content_navigation_components('ctxtoolbar') - @deprecated('[3.8] render_entity_metadata method is deprecated') - def render_entity_metadata(self, entity): - entity.view('metadata', w=self.w) - def summary(self, entity): """default implementation return an empty string""" return u'' @@ -217,16 +213,8 @@ if display_attributes: self.w(u'') for rschema, role, dispctrl, value in display_attributes: - # pylint: disable=E1101 - if not hasattr(self, '_render_attribute'): - label = self._rel_label(entity, rschema, role, dispctrl) - self.render_attribute(label, value, table=True) - else: - warn('[3.9] _render_attribute prototype has changed and ' - 'renamed to render_attribute, please update %s' - % self.__class__, DeprecationWarning) - self._render_attribute(dispctrl, rschema, value, role=role, - table=True) + label = self._rel_label(entity, rschema, role, dispctrl) + self.render_attribute(label, value, table=True) self.w(u'
    ') def render_attribute(self, label, value, table=False): @@ -253,13 +241,6 @@ rset = self._relation_rset(entity, rschema, role, dispctrl, limit=limit) if not rset: continue - if hasattr(self, '_render_relation'): - # pylint: disable=E1101 - self._render_relation(dispctrl, rset, 'autolimited') - warn('[3.9] _render_relation prototype has changed and has ' - 'been renamed to render_relation, please update %s' - % self.__class__, DeprecationWarning) - continue try: rview = self._cw.vreg['views'].select( vid, self._cw, rset=rset, dispctrl=dispctrl) diff -r cf27006ce813 -r b1e933b0e850 web/views/reledit.py --- a/web/views/reledit.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/reledit.py Mon Dec 09 16:13:10 2013 +0100 @@ -91,9 +91,6 @@ rschema = self._cw.vreg.schema[rtype] rctrl = self._cw.vreg['uicfg'].select('reledit', self._cw, entity=entity) self._rules = rctrl.etype_get(self.entity.e_schema.type, rschema.type, role, '*') - if rvid is not None or default_value is not None: - warn('[3.9] specifying rvid/default_value on select is deprecated, ' - 'reledit_ctrl rtag to control this' % self, DeprecationWarning) reload = self._compute_reload(rschema, role, reload) divid = self._build_divid(rtype, role, self.entity.eid) if rschema.final: @@ -322,19 +319,11 @@ rdef = entity.e_schema.rdef(rschema) return rdef.has_perm(self._cw, 'update', eid=entity.eid) - should_edit_attributes = deprecated('[3.9] should_edit_attributes is deprecated,' - ' use _should_edit_attribute instead', - _should_edit_attribute) - def _should_edit_relation(self, rschema, role): eeid = self.entity.eid perm_args = {'fromeid': eeid} if role == 'subject' else {'toeid': eeid} return rschema.has_perm(self._cw, 'add', **perm_args) - should_edit_relations = deprecated('[3.9] should_edit_relations is deprecated,' - ' use _should_edit_relation instead', - _should_edit_relation) - def _open_form_wrapper(self, divid, value, form, renderer, _edit_related, _add_related, _delete_related): w = self.w diff -r cf27006ce813 -r b1e933b0e850 web/views/schema.py --- a/web/views/schema.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/schema.py Mon Dec 09 16:13:10 2013 +0100 @@ -225,6 +225,7 @@ {'x': entity.eid}) self.wview('table', rset, 'null', cellvids={0: 'rdef-name-cell', + 2: 'etype-attr-defaultval-cell', 3: 'etype-attr-cardinality-cell', 4: 'rdef-constraints-cell', 6: 'rdef-options-cell'}, @@ -271,6 +272,14 @@ self.w(self._cw._(u'no')) +class CWETypeAttributeDefaultValCell(baseviews.FinalView): + __regid__ = 'etype-attr-defaultval-cell' + + def cell_call(self, row, col): + defaultval = self.cw_rset.rows[row][col] + if defaultval is not None: + self.w(unicode(self.cw_rset.rows[row][col].unzpickle())) + class CWETypeRelationCardinalityCell(baseviews.FinalView): __regid__ = 'etype-rel-cardinality-cell' diff -r cf27006ce813 -r b1e933b0e850 web/views/sessions.py --- a/web/views/sessions.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/sessions.py Mon Dec 09 16:13:10 2013 +0100 @@ -100,8 +100,6 @@ def _update_last_login_time(self, req): # XXX should properly detect missing permission / non writeable source # and avoid "except (RepositoryError, Unauthorized)" below - if req.user.cw_metainformation()['source']['type'] == 'ldapuser': - return try: req.execute('SET X last_login_time NOW WHERE X eid %(x)s', {'x' : req.user.eid}) diff -r cf27006ce813 -r b1e933b0e850 web/views/treeview.py --- a/web/views/treeview.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/treeview.py Mon Dec 09 16:13:10 2013 +0100 @@ -29,13 +29,26 @@ from cubicweb.utils import make_uid, json 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) +def _done_init(done, view, row, col): + """handle an infinite recursion safety belt""" + if done is None: + done = set() + entity = view.cw_rset.get_entity(row, col) + if entity.eid in done: + msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % { + 'rel': entity.cw_adapt_to('ITree').tree_relation, + 'eid': entity.eid + } + return None, msg + done.add(entity.eid) + return done, entity + class BaseTreeView(baseviews.ListView): """base tree view""" diff -r cf27006ce813 -r b1e933b0e850 web/views/urlrewrite.py --- a/web/views/urlrewrite.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/urlrewrite.py Mon Dec 09 16:13:10 2013 +0100 @@ -140,7 +140,7 @@ return None, None -def build_rset(rql, rgxgroups=None, cachekey=None, setuser=False, +def build_rset(rql, rgxgroups=None, setuser=False, vid=None, vtitle=None, form={}, **kwargs): def do_build_rset(inputurl, uri, req, schema, kwargs=kwargs): @@ -156,7 +156,7 @@ req.form['vid'] = vid if vtitle: req.form['vtitle'] = req._(vtitle) % kwargs - return None, req.execute(rql, kwargs, cachekey) + return None, req.execute(rql, kwargs) return do_build_rset def update_form(**kwargs): diff -r cf27006ce813 -r b1e933b0e850 web/views/xmlrss.py --- a/web/views/xmlrss.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/views/xmlrss.py Mon Dec 09 16:13:10 2013 +0100 @@ -28,7 +28,6 @@ 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 from cubicweb.uilib import simple_sgml_tag from cubicweb.web import httpcache, component @@ -185,7 +184,6 @@ __regid__ = 'IFeed' __select__ = is_instance('Any') - @implements_adapter_compat('IFeed') def rss_feed_url(self): """return an url to the rss feed for this entity""" return self.entity.absolute_url(vid='rss') diff -r cf27006ce813 -r b1e933b0e850 web/webconfig.py --- a/web/webconfig.py Fri Dec 06 17:20:59 2013 +0100 +++ b/web/webconfig.py Mon Dec 09 16:13:10 2013 +0100 @@ -403,7 +403,7 @@ self._load_ui_properties_file(uiprops, path) self._load_ui_properties_file(uiprops, self.apphome) datadir_url = uiprops.context['datadir_url'] - # XXX pre 3.9 css compat + # pre 3.9 css compat, however the old css still rules if self['use-old-css']: if (datadir_url+'/cubicweb.css') in uiprops['STYLESHEETS']: idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.css') @@ -415,21 +415,6 @@ uiprops['JAVASCRIPTS'].insert(0, cubicweb_js_url) def _load_ui_properties_file(self, uiprops, path): - resourcesfile = join(path, 'data', 'external_resources') - if exists(resourcesfile): - warn('[3.9] %s file is deprecated, use an uiprops.py file' - % resourcesfile, DeprecationWarning) - datadir_url = uiprops.context['datadir_url'] - for rid, val in read_config(resourcesfile).iteritems(): - if rid in ('STYLESHEETS', 'STYLESHEETS_PRINT', - 'IE_STYLESHEETS', 'JAVASCRIPTS'): - val = [w.strip().replace('DATADIR', datadir_url) - for w in val.split(',') if w.strip()] - if rid == 'IE_STYLESHEETS': - rid = 'STYLESHEETS_IE' - else: - val = val.strip().replace('DATADIR', datadir_url) - uiprops[rid] = val uipropsfile = join(path, 'uiprops.py') if exists(uipropsfile): self.debug('loading %s', uipropsfile) diff -r cf27006ce813 -r b1e933b0e850 wsgi/handler.py --- a/wsgi/handler.py Fri Dec 06 17:20:59 2013 +0100 +++ b/wsgi/handler.py Mon Dec 09 16:13:10 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -17,8 +17,6 @@ # with CubicWeb. If not, see . """WSGI request handler for cubicweb""" - - __docformat__ = "restructuredtext en" from itertools import chain, repeat, izip @@ -92,29 +90,21 @@ return iter(self.body) - class CubicWebWSGIApplication(object): """This is the wsgi application which will be called by the wsgi server with the WSGI ``environ`` and ``start_response`` parameters. - - XXX: missing looping tasks and proper repository shutdown when - the application is stopped. - NOTE: no pyro """ - def __init__(self, config, vreg=None): - self.appli = CubicWebPublisher(config, vreg=vreg) + def __init__(self, config): + self.appli = CubicWebPublisher(config) self.config = config self.base_url = self.config['base-url'] - self.https_url = self.config['https-url'] self.url_rewriter = self.appli.vreg['components'].select_or_none('urlrewriter') def _render(self, req): """this function performs the actual rendering """ - if self.base_url is None: - self.base_url = self.config._base_url = req.base_url() try: path = req.path result = self.appli.handle_request(req, path) diff -r cf27006ce813 -r b1e933b0e850 wsgi/server.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wsgi/server.py Mon Dec 09 16:13:10 2013 +0100 @@ -0,0 +1,46 @@ +# copyright 2003-2013 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 . +"""dummy wsgi server for CubicWeb web instances""" + +__docformat__ = "restructuredtext en" + +from cubicweb.wsgi.handler import CubicWebWSGIApplication +from cubicweb import ConfigurationError +from wsgiref import simple_server + +from logging import getLogger +LOGGER = getLogger('cubicweb') + + +def run(config): + config.check_writeable_uid_directory(config.appdatahome) + + port = config['port'] or 8080 + interface = config['interface'] + + app = CubicWebWSGIApplication(config) + handler_cls = simple_server.WSGIRequestHandler + httpd = simple_server.WSGIServer((interface, port), handler_cls) + httpd.set_app(app) + repo = app.appli.repo + try: + repo.start_looping_tasks() + LOGGER.info('starting http server on %s', config['base-url']) + httpd.serve_forever() + finally: + repo.shutdown() diff -r cf27006ce813 -r b1e933b0e850 wsgi/wz.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wsgi/wz.py Mon Dec 09 16:13:10 2013 +0100 @@ -0,0 +1,47 @@ +# copyright 2003-2013 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 . +"""dummy wsgi server for CubicWeb web instances""" + +__docformat__ = "restructuredtext en" + +import socket + +from cubicweb.wsgi.handler import CubicWebWSGIApplication +from cubicweb import ConfigurationError +from werkzeug.serving import run_simple + +from logging import getLogger +LOGGER = getLogger('cubicweb') + + +def run(config): + config.check_writeable_uid_directory(config.appdatahome) + + port = config['port'] or 8080 + interface = config['interface'] + + app = CubicWebWSGIApplication(config) + repo = app.appli.repo + try: + repo.start_looping_tasks() + run_simple(interface, port, app, + threaded=True, + use_debugger=True, + processes=1) # more processes yield weird errors + finally: + repo.shutdown()