# HG changeset patch # User Julien Cristau # Date 1389370340 -3600 # Node ID e83cbc116352cf4b24e59ef1986041ccb647bcba # Parent 96dba2efd16df59c30fa6b6d15b758d3f89fcfe7# Parent 2790fb8f7e037457f5b689f543fba6c6fa33cdf0 merge stable diff -r 2790fb8f7e03 -r e83cbc116352 README --- a/README Fri Jun 21 14:22:39 2013 +0200 +++ b/README Fri Jan 10 17:12:20 2014 +0100 @@ -34,4 +34,4 @@ Look in the doc/ subdirectory or read http://docs.cubicweb.org/ - +It includes the Entypo pictograms by Daniel Bruce — www.entypo.com diff -r 2790fb8f7e03 -r e83cbc116352 __init__.py --- a/__init__.py Fri Jun 21 14:22:39 2013 +0200 +++ b/__init__.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 __pkginfo__.py --- a/__pkginfo__.py Fri Jun 21 14:22:39 2013 +0200 +++ b/__pkginfo__.py Fri Jan 10 17:12:20 2014 +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', @@ -51,7 +51,7 @@ 'Twisted': '', # XXX graphviz # server dependencies - 'logilab-database': '>= 1.10', + 'logilab-database': '>= 1.11', 'passlib': '', } diff -r 2790fb8f7e03 -r e83cbc116352 _exceptions.py --- a/_exceptions.py Fri Jun 21 14:22:39 2013 +0200 +++ b/_exceptions.py Fri Jan 10 17:12:20 2014 +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. @@ -19,6 +19,10 @@ __docformat__ = "restructuredtext en" +from warnings import warn + +from logilab.common.decorators import cachedproperty + from yams import ValidationError as ValidationError # abstract exceptions ######################################################### @@ -81,6 +85,26 @@ class UniqueTogetherError(RepositoryError): """raised when a unique_together constraint caused an IntegrityError""" + def __init__(self, session, **kwargs): + self.session = session + assert 'rtypes' in kwargs or 'cstrname' in kwargs + self.kwargs = kwargs + + @cachedproperty + def rtypes(self): + if 'rtypes' in self.kwargs: + return self.kwargs['rtypes'] + cstrname = unicode(self.kwargs['cstrname']) + cstr = self.session.find('CWUniqueTogetherConstraint', name=cstrname).one() + return sorted(rtype.name for rtype in cstr.relations) + + @cachedproperty + def args(self): + warn('[3.18] UniqueTogetherError.args is deprecated, just use ' + 'the .rtypes accessor.', + DeprecationWarning) + # the first argument, etype, is never used and was never garanteed anyway + return None, self.rtypes # security exceptions ######################################################### @@ -134,6 +158,15 @@ a non final entity """ +class MultipleResultsError(CubicWebRuntimeError): + """raised when ResultSet.one() is called on a resultset with multiple rows + of multiple columns. + """ + +class NoResultError(CubicWebRuntimeError): + """raised when no result is found but at least one is expected. + """ + class UndoTransactionException(QueryError): """Raised when undoing a transaction could not be performed completely. diff -r 2790fb8f7e03 -r e83cbc116352 cubicweb.spec --- a/cubicweb.spec Fri Jun 21 14:22:39 2013 +0200 +++ b/cubicweb.spec Fri Jan 10 17:12:20 2014 +0100 @@ -20,11 +20,11 @@ BuildArch: noarch Requires: %{python} -Requires: %{python}-logilab-common >= 0.59.0 +Requires: %{python}-logilab-common >= 0.60.0 Requires: %{python}-logilab-mtconverter >= 0.8.0 Requires: %{python}-rql >= 0.31.2 -Requires: %{python}-yams >= 0.37.0 -Requires: %{python}-logilab-database >= 1.10.0 +Requires: %{python}-yams >= 0.39.0 +Requires: %{python}-logilab-database >= 1.11.0 Requires: %{python}-passlib Requires: %{python}-lxml Requires: %{python}-twisted-web diff -r 2790fb8f7e03 -r e83cbc116352 cwconfig.py --- a/cwconfig.py Fri Jun 21 14:22:39 2013 +0200 +++ b/cwconfig.py Fri Jan 10 17:12:20 2014 +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 @@ -1189,11 +1177,13 @@ sourcedirs.append(self.i18n_lib_dir()) return i18n.compile_i18n_catalogs(sourcedirs, i18ndir, langs) - def sendmails(self, msgs): + def sendmails(self, msgs, fromaddr=None): """msgs: list of 2-uple (message object, recipients). Return False if connection to the smtp server failed, else True. """ server, port = self['smtp-host'], self['smtp-port'] + if fromaddr is None: + fromaddr = '%s <%s>' % (self['sender-name'], self['sender-addr']) SMTP_LOCK.acquire() try: try: @@ -1202,10 +1192,9 @@ self.exception("can't connect to smtp server %s:%s (%s)", server, port, ex) return False - heloaddr = '%s <%s>' % (self['sender-name'], self['sender-addr']) for msg, recipients in msgs: try: - smtp.sendmail(heloaddr, recipients, msg.as_string()) + smtp.sendmail(fromaddr, recipients, msg.as_string()) except Exception as ex: self.exception("error sending mail to %s (%s)", recipients, ex) diff -r 2790fb8f7e03 -r e83cbc116352 cwctl.py --- a/cwctl.py Fri Jun 21 14:22:39 2013 +0200 +++ b/cwctl.py Fri Jan 10 17:12:20 2014 +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. @@ -48,13 +48,9 @@ from cubicweb.toolsutils import Command, rm, create_dir, underline_title from cubicweb.__pkginfo__ import version -if support_args(CommandLine, 'check_duplicated_command'): - # don't check duplicated commands, it occurs when reloading site_cubicweb - CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.', - version=version, check_duplicated_command=False) -else: - CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.', - version=version) +# don't check duplicated commands, it occurs when reloading site_cubicweb +CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.', + version=version, check_duplicated_command=False) def wait_process_end(pid, maxtry=10, waittime=1): """wait for a process to actually die""" @@ -266,12 +262,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 +348,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' @@ -761,6 +752,7 @@ mih = config.migration_handler() repo = mih.repo_connect() vcconf = repo.get_versions() + helper = self.config_helper(config, required=False) if self.config.force_cube_version: for cube, version in self.config.force_cube_version.iteritems(): vcconf[cube] = Version(version) @@ -799,6 +791,8 @@ if not self.i18nupgrade(config): return print + if helper: + helper.postupgrade(repo) print '-> instance migrated.' if instance_running and not (CWDEV or self.config.nostartstop): # restart instance through fork to get a proper environment, avoid @@ -1039,9 +1033,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 +1093,8 @@ ): CWCTL.register(cmdcls) + + def run(args): """command line tool""" import os diff -r 2790fb8f7e03 -r e83cbc116352 cwvreg.py --- a/cwvreg.py Fri Jun 21 14:22:39 2013 +0200 +++ b/cwvreg.py Fri Jan 10 17:12:20 2014 +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 @@ -626,6 +615,15 @@ self.register_objects(path) CW_EVENT_MANAGER.emit('after-registry-reload') + def load_file(self, filepath, modname): + # override to allow some instrumentation (eg localperms) + modpath = modname.split('.') + try: + self.currently_loading_cube = modpath[modpath.index('cubes') + 1] + except ValueError: + self.currently_loading_cube = 'cubicweb' + return super(CWRegistryStore, self).load_file(filepath, modname) + def _set_schema(self, schema): """set instance'schema""" self.schema = schema @@ -641,20 +639,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 +649,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 +662,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 +688,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 2790fb8f7e03 -r e83cbc116352 dataimport.py --- a/dataimport.py Fri Jun 21 14:22:39 2013 +0200 +++ b/dataimport.py Fri Jan 10 17:12:20 2014 +0100 @@ -802,6 +802,9 @@ assert not rtype.startswith('reverse_') self.add_relation(self.session, eid_from, rtype, eid_to, self.rschema(rtype).inlined) + if self.rschema[rtype].symmetric: + self.add_relation(self.session, eid_to, rtype, eid_from, + self.rschema(rtype).inlined) self._nb_inserted_relations += 1 @property @@ -928,6 +931,9 @@ # XXX Could subjtype be inferred ? self.source.add_relation(self.session, subj_eid, rtype, obj_eid, self.rschema(rtype).inlined, **kwargs) + if self.rschema[rtype].symmetric: + self.source.add_relation(self.session, obj_eid, rtype, subj_eid, + self.rschema(rtype).inlined, **kwargs) def drop_indexes(self, etype): """Drop indexes for a given entity type""" diff -r 2790fb8f7e03 -r e83cbc116352 dbapi.py --- a/dbapi.py Fri Jun 21 14:22:39 2013 +0200 +++ b/dbapi.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 debian/control --- a/debian/control Fri Jun 21 14:22:39 2013 +0200 +++ b/debian/control Fri Jan 10 17:12:20 2014 +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 @@ -51,7 +51,7 @@ ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), - python-logilab-database (>= 1.10.0), + python-logilab-database (>= 1.11.0), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2, @@ -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. . @@ -149,8 +151,8 @@ graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), - python-logilab-common (>= 0.59.0), - python-yams (>= 0.37.0), + python-logilab-common (>= 0.60.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 2790fb8f7e03 -r e83cbc116352 debian/copyright --- a/debian/copyright Fri Jun 21 14:22:39 2013 +0200 +++ b/debian/copyright Fri Jan 10 17:12:20 2014 +0100 @@ -8,7 +8,7 @@ Copyright: - Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE). + Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). http://www.logilab.fr/ -- mailto:contact@logilab.fr License: @@ -29,3 +29,18 @@ On Debian systems, the complete text of the GNU Lesser General Public License may be found in '/usr/share/common-licenses/LGPL-2.1'. + +Entypo pictograms: + +Author: + + Daniel Bruce (www.entypo.com) + +Licence: + + Entypo pictograms are licensed under CC BY-SA 3.0 and the font + under SIL Open Font License. + + The rights to each pictogram in the social extension are either + trademarked or copyrighted by the respective company. + diff -r 2790fb8f7e03 -r e83cbc116352 devtools/__init__.py --- a/devtools/__init__.py Fri Jun 21 14:22:39 2013 +0200 +++ b/devtools/__init__.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 devtools/devctl.py --- a/devtools/devctl.py Fri Jun 21 14:22:39 2013 +0200 +++ b/devtools/devctl.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 devtools/instrument.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/instrument.py Fri Jan 10 17:12:20 2014 +0100 @@ -0,0 +1,224 @@ +# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program 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. +# +# This program 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 this program. If not, see . +"""Instrumentation utilities""" + +import os + +try: + import pygraphviz +except ImportError: + pygraphviz = None + +from cubicweb.cwvreg import CWRegistryStore +from cubicweb.devtools.devctl import DevConfiguration + + +ALL_COLORS = [ + "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", "000000", + "800000", "008000", "000080", "808000", "800080", "008080", "808080", + "C00000", "00C000", "0000C0", "C0C000", "C000C0", "00C0C0", "C0C0C0", + "400000", "004000", "000040", "404000", "400040", "004040", "404040", + "200000", "002000", "000020", "202000", "200020", "002020", "202020", + "600000", "006000", "000060", "606000", "600060", "006060", "606060", + "A00000", "00A000", "0000A0", "A0A000", "A000A0", "00A0A0", "A0A0A0", + "E00000", "00E000", "0000E0", "E0E000", "E000E0", "00E0E0", "E0E0E0", + ] +_COLORS = {} +def get_color(key): + try: + return _COLORS[key] + except KeyError: + _COLORS[key] = '#'+ALL_COLORS[len(_COLORS) % len(ALL_COLORS)] + return _COLORS[key] + +def warn(msg, *args): + print 'WARNING: %s' % (msg % args) + +def info(msg): + print 'INFO: ' + msg + + +class PropagationAnalyzer(object): + """Abstract propagation analyzer, providing utility function to extract + entities involved in propagation from a schema, as well as propagation + rules from hooks (provided they use intrumentalized sets, see + :class:`CubeTracerSet`). + + Concrete classes should at least define `prop_rel` class attribute and + implements the `is_root` method. + + See `localperms` or `nosylist` cubes for example usage (`ccplugin` module). + """ + prop_rel = None # name of the propagation relation + + def init(self, cube): + """Initialize analyze for the given cube, returning the (already loaded) + vregistry and a set of entities which we're interested in. + """ + config = DevConfiguration(cube) + schema = config.load_schema() + vreg = CWRegistryStore(config) + vreg.set_schema(schema) # set_schema triggers objects registrations + eschemas = set(eschema for eschema in schema.entities() + if self.should_include(eschema)) + return vreg, eschemas + + def is_root(self, eschema): + """Return `True` if given entity schema is a root of the graph""" + raise NotImplementedError() + + def should_include(self, eschema): + """Return `True` if given entity schema should be included by the graph. + """ + + if self.prop_rel in eschema.subjrels or self.is_root(eschema): + return True + return False + + def prop_edges(self, s_rels, o_rels, eschemas): + """Return a set of edges where propagation has been detected. + + Each edge is defined by a 4-uple (from node, to node, rtype, package) + where `rtype` is the relation type bringing from to and `package` is the cube adding the rule to the propagation + control set (see see :class:`CubeTracerSet`). + """ + schema = iter(eschemas).next().schema + prop_edges = set() + for rtype in s_rels: + found = False + for subj, obj in schema.rschema(rtype).rdefs: + if subj in eschemas and obj in eschemas: + found = True + prop_edges.add( (subj, obj, rtype, s_rels.value_cube[rtype]) ) + if not found: + warn('no rdef match for %s', rtype) + for rtype in o_rels: + found = False + for subj, obj in schema.rschema(rtype).rdefs: + if subj in eschemas and obj in eschemas: + found = True + prop_edges.add( (obj, subj, rtype, o_rels.value_cube[rtype]) ) + if not found: + warn('no rdef match for %s', rtype) + return prop_edges + + def detect_problems(self, eschemas, edges): + """Given the set of analyzed entity schemas and edges between them, + return a set of entity schemas where a problem has been detected. + """ + problematic = set() + for eschema in eschemas: + if self.has_problem(eschema, edges): + problematic.add(eschema) + not_problematic = set(eschemas).difference(problematic) + if not_problematic: + info('nothing problematic in: %s' % + ', '.join(e.type for e in not_problematic)) + return problematic + + def has_problem(self, eschema, edges): + """Return `True` if the given schema is considered problematic, + considering base propagation rules. + """ + root = self.is_root(eschema) + has_prop_rel = self.prop_rel in eschema.subjrels + # root but no propagation relation + if root and not has_prop_rel: + warn('%s is root but miss %s', eschema, self.prop_rel) + return True + # propagated but without propagation relation / not propagated but + # with propagation relation + if not has_prop_rel and \ + any(edge for edge in edges if edge[1] == eschema): + warn("%s miss %s but is reached by propagation", + eschema, self.prop_rel) + return True + elif has_prop_rel and not root: + rdef = eschema.rdef(self.prop_rel, takefirst=True) + edges = [edge for edge in edges if edge[1] == eschema] + if not edges: + warn("%s has %s but isn't reached by " + "propagation", eschema, self.prop_rel) + return True + # require_permission relation / propagation rule not added by + # the same cube + elif not any(edge for edge in edges if edge[-1] == rdef.package): + warn('%s has %s relation / propagation rule' + ' not added by the same cube (%s / %s)', eschema, + self.prop_rel, rdef.package, edges[0][-1]) + return True + return False + + def init_graph(self, eschemas, edges, problematic): + """Initialize and return graph, adding given nodes (entity schemas) and + edges between them. + + Require pygraphviz installed. + """ + if pygraphviz is None: + raise RuntimeError('pygraphviz is not installed') + graph = pygraphviz.AGraph(strict=False, directed=True) + for eschema in eschemas: + if eschema in problematic: + params = {'color': '#ff0000', 'fontcolor': '#ff0000'} + else: + params = {}#'color': get_color(eschema.package)} + graph.add_node(eschema.type, **params) + for subj, obj, rtype, package in edges: + graph.add_edge(str(subj), str(obj), label=rtype, + color=get_color(package)) + return graph + + def add_colors_legend(self, graph): + """Add a legend of used colors to the graph.""" + for package, color in sorted(_COLORS.iteritems()): + graph.add_node(package, color=color, fontcolor=color, shape='record') + + +class CubeTracerSet(object): + """Dumb set implementation whose purpose is to keep track of which cube is + being loaded when something is added to the set. + + Results will be found in the `value_cube` attribute dictionary. + + See `localperms` or `nosylist` cubes for example usage (`hooks` module). + """ + def __init__(self, vreg, wrapped): + self.vreg = vreg + self.wrapped = wrapped + self.value_cube = {} + + def add(self, value): + self.wrapped.add(value) + cube = self.vreg.currently_loading_cube + if value in self.value_cube: + warn('%s is propagated by cube %s and cube %s', + value, self.value_cube[value], cube) + else: + self.value_cube[value] = cube + + def __iter__(self): + return iter(self.wrapped) + + def __ior__(self, other): + for value in other: + self.add(value) + return self + + def __ror__(self, other): + other |= self.wrapped + return other diff -r 2790fb8f7e03 -r e83cbc116352 devtools/testlib.py --- a/devtools/testlib.py Fri Jun 21 14:22:39 2013 +0200 +++ b/devtools/testlib.py Fri Jan 10 17:12:20 2014 +0100 @@ -89,7 +89,7 @@ MAILBOX = [] -class Email: +class Email(object): """you'll get instances of Email into MAILBOX during tests that trigger some notification. @@ -98,7 +98,8 @@ * `recipients` is a list of email address which are the recipients of this message """ - def __init__(self, recipients, msg): + def __init__(self, fromaddr, recipients, msg): + self.fromaddr = fromaddr self.recipients = recipients self.msg = msg @@ -125,8 +126,8 @@ pass def close(self): pass - def sendmail(self, helo_addr, recipients, msg): - MAILBOX.append(Email(recipients, msg)) + def sendmail(self, fromaddr, recipients, msg): + MAILBOX.append(Email(fromaddr, recipients, msg)) cwconfig.SMTP = MockSMTP @@ -419,13 +420,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 +445,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 +981,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 2790fb8f7e03 -r e83cbc116352 doc/3.15.rst --- a/doc/3.15.rst Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/3.15.rst Fri Jan 10 17:12:20 2014 +0100 @@ -13,7 +13,7 @@ used for entity cache invalidation. * Improved WSGI support. While there is still some caveats, most of the code - which as twisted only is now generic and allows related functionalities to work + which was twisted only is now generic and allows related functionalities to work with a WSGI front-end. * Full undo/transaction support : undo of modification has eventually been diff -r 2790fb8f7e03 -r e83cbc116352 doc/3.18.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/3.18.rst Fri Jan 10 17:12:20 2014 +0100 @@ -0,0 +1,101 @@ +What's new in CubicWeb 3.18? +============================ + +The migration script does not handle sqlite nor mysql instances. + + +New functionalities +-------------------- + +* add a security debugging tool + (see `#2920304 `_) + +* introduce an `add` permission on attributes, to be interpreted at + entity creation time only and allow the implementation of complex + `update` rules that don't block entity creation (before that the + `update` attribute permission was interpreted at entity creation and + update time) + +* the primary view display controller (uicfg) now has a + `set_fields_order` method similar to the one available for forms + +* new method `ResultSet.one(col=0)` to retrive a single entity and enforce the + result has only one row (see `#3352314 https://www.cubicweb.org/ticket/3352314`_) + +* new method `RequestSessionBase.find` to look for entities + (see `#3361290 https://www.cubicweb.org/ticket/3361290`_) + +* the embedded jQuery copy has been updated to version 1.10.2, and jQuery UI to + version 1.10.3. + +* initial support for wsgi for the debug mode, available through the new + ``wsgi`` cubicweb-ctl command, which can use either python's builtin + wsgi server or the werkzeug module if present. + +* a ``rql-table`` directive is now available in ReST fields + +* cubicweb-ctl upgrade can now generate the static data resource directory + directly, without a manual call to gen-static-datadir. + +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. + +* `symmetric` relations are no more handled by an rql rewrite but are + now handled with hooks (from the `activeintegrity` category); this + may have some consequences for applications that do low-level database + manipulations or at times disable (some) hooks. + +* `unique together` constraints (multi-columns unicity constraints) + get a `name` attribute that maps the CubicWeb contraint entities to + corresponding backend index. + +* BreadCrumbEntityVComponent's open_breadcrumbs method now includes + the first breadcrumbs separator + +* entities can be compared for equality and hashed + +* the ``on_fire_transition`` predicate accepts a sequence of possible + transition names + +* the GROUP_CONCAT rql aggregate function no longer repeats duplicate + values, on the sqlite and postgresql backends + +Deprecation +--------------------- + +* ``pyrorql`` sources have been deprecated. Multisource will be fully dropped + in the next version. If you are still using pyrorql, switch to ``datafeed`` + **NOW**! + +* the old multi-source system + +* `find_one_entity` and `find_entities` in favor of `find` + (see `#3361290 https://www.cubicweb.org/ticket/3361290`_) + +* the `TmpFileViewMixin` and `TmpPngView` classes (see `#3400448 + https://www.cubicweb.org/ticket/3400448`_) + +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 + +* the ``twisted`` (web-only) instance type has been removed diff -r 2790fb8f7e03 -r e83cbc116352 doc/book/en/admin/config.rst --- a/doc/book/en/admin/config.rst Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/book/en/admin/config.rst Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 doc/book/en/admin/setup.rst --- a/doc/book/en/admin/setup.rst Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/book/en/admin/setup.rst Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 doc/book/en/devrepo/datamodel/definition.rst --- a/doc/book/en/devrepo/datamodel/definition.rst Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/book/en/devrepo/datamodel/definition.rst Fri Jan 10 17:12:20 2014 +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 @@ -330,7 +330,8 @@ For a relation, the possible actions are `read`, `add`, and `delete`. -For an attribute, the possible actions are `read`, and `update`. +For an attribute, the possible actions are `read`, `add` and `update`, +and they are a refinement of an entity type permission. For each access type, a tuple indicates the name of the authorized groups and/or one or multiple RQL expressions to satisfy to grant access. The access is @@ -364,7 +365,8 @@ .. sourcecode:: python __permissions__ = {'read': ('managers', 'users', 'guests',), - 'update': ('managers', ERQLExpression('U has_update_permission X')),} + 'add': ('managers', ERQLExpression('U has_add_permission X'), + 'update': ('managers', ERQLExpression('U has_update_permission X')),} The standard user groups ```````````````````````` @@ -476,13 +478,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 2790fb8f7e03 -r e83cbc116352 doc/book/en/devrepo/entityclasses/adapters.rst --- a/doc/book/en/devrepo/entityclasses/adapters.rst Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/book/en/devrepo/entityclasses/adapters.rst Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 doc/book/en/devrepo/migration.rst --- a/doc/book/en/devrepo/migration.rst Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/book/en/devrepo/migration.rst Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 doc/book/en/devrepo/testing.rst --- a/doc/book/en/devrepo/testing.rst Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/book/en/devrepo/testing.rst Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 doc/book/en/tutorials/advanced/part02_security.rst --- a/doc/book/en/tutorials/advanced/part02_security.rst Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/book/en/tutorials/advanced/part02_security.rst Fri Jan 10 17:12:20 2014 +0100 @@ -259,26 +259,26 @@ # relations where the "parent" entity is the object O_RELS = set(('filed_under', 'comments',)) - class AddEntitySecurityPropagationHook(hook.PropagateSubjectRelationHook): + class AddEntitySecurityPropagationHook(hook.PropagateRelationHook): """propagate permissions when new entity are added""" __regid__ = 'sytweb.addentity_security_propagation' - __select__ = (hook.PropagateSubjectRelationHook.__select__ + __select__ = (hook.PropagateRelationHook.__select__ & hook.match_rtype_sets(S_RELS, O_RELS)) main_rtype = 'may_be_read_by' subject_relations = S_RELS object_relations = O_RELS - class AddPermissionSecurityPropagationHook(hook.PropagateSubjectRelationAddHook): + class AddPermissionSecurityPropagationHook(hook.PropagateRelationAddHook): """propagate permissions when new entity are added""" __regid__ = 'sytweb.addperm_security_propagation' - __select__ = (hook.PropagateSubjectRelationAddHook.__select__ + __select__ = (hook.PropagateRelationAddHook.__select__ & hook.match_rtype('may_be_read_by',)) subject_relations = S_RELS object_relations = O_RELS - class DelPermissionSecurityPropagationHook(hook.PropagateSubjectRelationDelHook): + class DelPermissionSecurityPropagationHook(hook.PropagateRelationDelHook): __regid__ = 'sytweb.delperm_security_propagation' - __select__ = (hook.PropagateSubjectRelationDelHook.__select__ + __select__ = (hook.PropagateRelationDelHook.__select__ & hook.match_rtype('may_be_read_by',)) subject_relations = S_RELS object_relations = O_RELS diff -r 2790fb8f7e03 -r e83cbc116352 doc/book/en/tutorials/advanced/part04_ui-base.rst --- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 doc/book/en/tutorials/advanced/part05_ui-advanced.rst --- a/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 doc/tools/pyjsrest.py --- a/doc/tools/pyjsrest.py Fri Jun 21 14:22:39 2013 +0200 +++ b/doc/tools/pyjsrest.py Fri Jan 10 17:12:20 2014 +0100 @@ -136,7 +136,6 @@ 'cubicweb.preferences', 'cubicweb.edition', 'cubicweb.reledit', - 'cubicweb.rhythm', 'cubicweb.timeline-ext', ] diff -r 2790fb8f7e03 -r e83cbc116352 entities/adapters.py --- a/entities/adapters.py Fri Jun 21 14:22:39 2013 +0200 +++ b/entities/adapters.py Fri Jan 10 17:12:20 2014 +0100 @@ -1,4 +1,4 @@ -# copyright 2010-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2010-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -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.""" @@ -389,23 +362,12 @@ __select__ = match_exception(UniqueTogetherError) def raise_user_exception(self): - etype, rtypes = self.exc.args - # Because of index name size limits (e.g: postgres around 64, - # sqlserver around 128), we cannot be sure of what we got, - # especially for the rtypes part. - # Hence we will try to validate them, and handle invalid ones - # in the most user-friendly manner ... _ = self._cw._ - schema = self.entity._cw.vreg.schema + rtypes = self.exc.rtypes rtypes_msg = {} for rtype in rtypes: - if rtype in schema: - rtypes_msg[rtype] = _('%s is part of violated unicity constraint') % rtype - globalmsg = _('some relations %sviolate a unicity constraint') - if len(rtypes) != len(rtypes_msg): # we got mangled/missing rtypes - globalmsg = globalmsg % _('(not all shown here) ') - else: - globalmsg = globalmsg % '' + rtypes_msg[rtype] = _('%s is part of violated unicity constraint') % rtype + globalmsg = _('some relations violate a unicity constraint') rtypes_msg['unicity constraint'] = globalmsg raise ValidationError(self.entity.eid, rtypes_msg) diff -r 2790fb8f7e03 -r e83cbc116352 entities/test/unittest_base.py --- a/entities/test/unittest_base.py Fri Jun 21 14:22:39 2013 +0200 +++ b/entities/test/unittest_base.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 entities/wfobjs.py --- a/entities/wfobjs.py Fri Jun 21 14:22:39 2013 +0200 +++ b/entities/wfobjs.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 entity.py --- a/entity.py Fri Jun 21 14:22:39 2013 +0200 +++ b/entity.py Fri Jan 10 17:12:20 2014 +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',) @@ -308,7 +287,10 @@ select._varmaker = rqlvar_maker(defined=select.defined_vars, aliases=select.aliases, index=26) if settype: - select.add_type_restriction(mainvar, cls.__regid__) + rel = select.add_type_restriction(mainvar, cls.__regid__) + # should use 'is_instance_of' instead of 'is' so we retrieve + # subclasses instances as well + rel.r_type = 'is_instance_of' if fetchattrs is None: fetchattrs = cls.fetch_attrs cls._fetch_restrictions(mainvar, select, fetchattrs, user, ordermethod) @@ -558,7 +540,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 +972,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 +986,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 +1000,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 +1008,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 +1332,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 2790fb8f7e03 -r e83cbc116352 etwist/request.py --- a/etwist/request.py Fri Jun 21 14:22:39 2013 +0200 +++ b/etwist/request.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 etwist/server.py --- a/etwist/server.py Fri Jun 21 14:22:39 2013 +0200 +++ b/etwist/server.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 etwist/twconfig.py --- a/etwist/twconfig.py Fri Jun 21 14:22:39 2013 +0200 +++ b/etwist/twconfig.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 etwist/twctl.py --- a/etwist/twctl.py Fri Jun 21 14:22:39 2013 +0200 +++ b/etwist/twctl.py Fri Jan 10 17:12:20 2014 +0100 @@ -22,7 +22,7 @@ from logilab.common.shellutils import rm from cubicweb.toolsutils import CommandHandler -from cubicweb.web.webctl import WebCreateHandler +from cubicweb.web.webctl import WebCreateHandler, WebUpgradeHandler # trigger configuration registration import cubicweb.etwist.twconfig # pylint: disable=W0611 @@ -48,6 +48,9 @@ def poststop(self): pass +class TWUpgradeHandler(WebUpgradeHandler): + cfgname = 'twisted' + try: from cubicweb.server import serverctl @@ -73,5 +76,8 @@ cfgname = 'all-in-one' subcommand = 'cubicweb-twisted' + class AllInOneUpgradeHandler(TWUpgradeHandler): + cfgname = 'all-in-one' + except ImportError: pass diff -r 2790fb8f7e03 -r e83cbc116352 ext/rest.py --- a/ext/rest.py Fri Jun 21 14:22:39 2013 +0200 +++ b/ext/rest.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 ext/test/data/views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ext/test/data/views.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 ext/test/unittest_rest.py --- a/ext/test/unittest_rest.py Fri Jun 21 14:22:39 2013 +0200 +++ b/ext/test/unittest_rest.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 hooks/integrity.py --- a/hooks/integrity.py Fri Jun 21 14:22:39 2013 +0200 +++ b/hooks/integrity.py Fri Jan 10 17:12:20 2014 +0100 @@ -109,6 +109,30 @@ category = 'integrity' +class EnsureSymmetricRelationsAdd(hook.Hook): + """ ensure X r Y => Y r X iff r is symmetric """ + __regid__ = 'cw.add_ensure_symmetry' + category = 'activeintegrity' + events = ('after_add_relation',) + # __select__ is set in the registration callback + + def __call__(self): + self._cw.repo.system_source.add_relation(self._cw, self.eidto, + self.rtype, self.eidfrom) + + +class EnsureSymmetricRelationsDelete(hook.Hook): + """ ensure X r Y => Y r X iff r is symmetric """ + __regid__ = 'cw.delete_ensure_symmetry' + category = 'activeintegrity' + events = ('after_delete_relation',) + # __select__ is set in the registration callback + + def __call__(self): + self._cw.repo.system_source.delete_relation(self._cw, self.eidto, + self.rtype, self.eidfrom) + + class CheckCardinalityHookBeforeDeleteRelation(IntegrityHook): """check cardinalities are satisfied""" __regid__ = 'checkcard_before_delete_relation' @@ -348,3 +372,11 @@ elif composite == 'object': _DelayedDeleteSEntityOp.get_instance(self._cw).add_data( (self.eidfrom, rtype)) + +def registration_callback(vreg): + vreg.register_all(globals().values(), __name__) + symmetric_rtypes = [rschema.type for rschema in vreg.schema.relations() + if rschema.symmetric] + EnsureSymmetricRelationsAdd.__select__ = hook.Hook.__select__ & hook.match_rtype(*symmetric_rtypes) + EnsureSymmetricRelationsDelete.__select__ = hook.Hook.__select__ & hook.match_rtype(*symmetric_rtypes) + diff -r 2790fb8f7e03 -r e83cbc116352 hooks/metadata.py --- a/hooks/metadata.py Fri Jun 21 14:22:39 2013 +0200 +++ b/hooks/metadata.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 hooks/security.py --- a/hooks/security.py Fri Jun 21 14:22:39 2013 +0200 +++ b/hooks/security.py Fri Jan 10 17:12:20 2014 +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. @@ -20,6 +20,7 @@ """ __docformat__ = "restructuredtext en" +from warnings import warn from logilab.common.registry import objectify_predicate @@ -29,8 +30,8 @@ from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook -_DEFAULT_UPDATE_ATTRPERM = buildobjs.DEFAULT_ATTRPERMS['update'] -def check_entity_attributes(session, entity, editedattrs=None, creation=False): + +def check_entity_attributes(session, entity, action, editedattrs=None): eid = entity.eid eschema = entity.e_schema # ._cw_skip_security_attributes is there to bypass security for attributes @@ -43,27 +44,25 @@ continue rdef = eschema.rdef(attr) if rdef.final: # non final relation are checked by standard hooks - # attributes only have a specific 'update' permission - updateperm = rdef.permissions.get('update') + perms = rdef.permissions.get(action) # comparison below works because the default update perm is: # - # ('managers', ERQLExpression(Any X WHERE U has_update_permission X, X eid %(x)s, U eid %(u)s)) + # ('managers', ERQLExpression(Any X WHERE U has_update_permission X, + # X eid %(x)s, U eid %(u)s)) # # is deserialized in this order (groups first), and ERQLExpression - # implements comparison by expression. - if updateperm == _DEFAULT_UPDATE_ATTRPERM: - # The default update permission is to delegate to the entity - # update permission. This is an historical artefact but it is - # costly (in general). Hence we take this permission object as a - # marker saying "no specific" update permissions for this - # attribute. Thus we just do nothing. + # implements comparison by rql expression. + if perms == buildobjs.DEFAULT_ATTRPERMS[action]: + # The default rule is to delegate to the entity + # rule. This is an historical artefact. Hence we take + # this object as a marker saying "no specific" + # permission rule for this attribute. Thus we just do + # nothing. continue - if creation and updateperm == (): - # That actually means an immutable attribute. We make an - # _exception_ to the `check attr update perms at entity create & - # update time` rule for this case. - continue - rdef.check_perm(session, 'update', eid=eid) + if perms == (): + # That means an immutable attribute. + raise Unauthorized(action, str(rdef)) + rdef.check_perm(session, action, eid=eid) class CheckEntityPermissionOp(hook.DataOperationMixIn, hook.LateOperation): @@ -72,8 +71,7 @@ for eid, action, edited in self.get_data(): entity = session.entity_from_eid(eid) entity.cw_check_perm(action) - check_entity_attributes(session, entity, edited, - creation=(action == 'add')) + check_entity_attributes(session, entity, action, edited) class CheckRelationPermissionOp(hook.DataOperationMixIn, hook.LateOperation): @@ -111,17 +109,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 2790fb8f7e03 -r e83cbc116352 hooks/syncschema.py --- a/hooks/syncschema.py Fri Jun 21 14:22:39 2013 +0200 +++ b/hooks/syncschema.py Fri Jan 10 17:12:20 2014 +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')) @@ -116,7 +101,7 @@ if (specialization, rdefdef.object) in rschema.rdefs: continue sperdef = RelationDefinitionSchema(specialization, rschema, - object, props) + object, None, values=props) ss.execschemarql(session.execute, sperdef, ss.rdef2rql(sperdef, cstrtypemap, groupmap)) @@ -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 @@ -738,44 +714,38 @@ class CWUniqueTogetherConstraintAddOp(MemSchemaOperation): entity = None # make pylint happy + def precommit_event(self): session = self.session prefix = SQL_PREFIX - table = '%s%s' % (prefix, self.entity.constraint_of[0].name) - cols = ['%s%s' % (prefix, r.name) for r in self.entity.relations] - dbhelper= session.cnxset.source('system').dbhelper - sqls = dbhelper.sqls_create_multicol_unique_index(table, cols) + entity = self.entity + table = '%s%s' % (prefix, entity.constraint_of[0].name) + cols = ['%s%s' % (prefix, r.name) for r in entity.relations] + dbhelper = session.cnxset.source('system').dbhelper + sqls = dbhelper.sqls_create_multicol_unique_index(table, cols, entity.name) for sql in sqls: session.system_sql(sql) - # XXX revertprecommit_event - def postcommit_event(self): - eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid) - attrs = [r.name for r in self.entity.relations] + entity = self.entity + eschema = self.session.vreg.schema.schema_by_eid(entity.constraint_of[0].eid) + attrs = [r.name for r in entity.relations] eschema._unique_together.append(attrs) class CWUniqueTogetherConstraintDelOp(MemSchemaOperation): - entity = oldcstr = None # for pylint - cols = [] # for pylint + entity = cstrname = None # for pylint + cols = () # for pylint + def precommit_event(self): session = self.session prefix = SQL_PREFIX table = '%s%s' % (prefix, self.entity.type) - dbhelper= session.cnxset.source('system').dbhelper + dbhelper = session.cnxset.source('system').dbhelper cols = ['%s%s' % (prefix, c) for c in self.cols] - sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols) + sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols, self.cstrname) for sql in sqls: - try: - session.system_sql(sql) - except Exception as exc: # should be ProgrammingError - if sql.startswith('DROP'): - self.error('execute of `%s` failed (cause: %s)', sql, exc) - continue - raise - - # XXX revertprecommit_event + session.system_sql(sql) def postcommit_event(self): eschema = self.session.vreg.schema.schema_by_eid(self.entity.eid) @@ -1195,9 +1165,9 @@ schema = self._cw.vreg.schema cstr = self._cw.entity_from_eid(self.eidfrom) entity = schema.schema_by_eid(self.eidto) - cols = [r.name for r in cstr.relations] + cols = tuple(r.name for r in cstr.relations) CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, - oldcstr=cstr, cols=cols) + cstrname=cstr.name, cols=cols) # permissions synchronization hooks ############################################ diff -r 2790fb8f7e03 -r e83cbc116352 hooks/test/data/schema.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hooks/test/data/schema.py Fri Jan 10 17:12:20 2014 +0100 @@ -0,0 +1,25 @@ +# 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 . + +from yams.buildobjs import RelationDefinition + +class friend(RelationDefinition): + subject = ('CWUser', 'CWGroup') + object = ('CWUser', 'CWGroup') + symmetric = True + diff -r 2790fb8f7e03 -r e83cbc116352 hooks/test/unittest_hooks.py --- a/hooks/test/unittest_hooks.py Fri Jun 21 14:22:39 2013 +0200 +++ b/hooks/test/unittest_hooks.py Fri Jan 10 17:12:20 2014 +0100 @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# 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. @@ -39,6 +39,37 @@ rset = self.execute('Any S WHERE X sender S, X eid %s' % eeid) self.assertEqual(len(rset), 1) + def test_symmetric(self): + req = self.request() + u1 = self.create_user(req, u'1') + u2 = self.create_user(req, u'2') + u3 = self.create_user(req, u'3') + ga = req.create_entity('CWGroup', name=u'A') + gb = req.create_entity('CWGroup', name=u'B') + u1.cw_set(friend=u2) + u2.cw_set(friend=u3) + ga.cw_set(friend=gb) + ga.cw_set(friend=u1) + self.commit() + req = self.request() + for l1, l2 in ((u'1', u'2'), + (u'2', u'3')): + self.assertTrue(req.execute('Any U1,U2 WHERE U1 friend U2, U1 login %(l1)s, U2 login %(l2)s', + {'l1': l1, 'l2': l2})) + self.assertTrue(req.execute('Any U1,U2 WHERE U2 friend U1, U1 login %(l1)s, U2 login %(l2)s', + {'l1': l1, 'l2': l2})) + self.assertTrue(req.execute('Any GA,GB WHERE GA friend GB, GA name "A", GB name "B"')) + self.assertTrue(req.execute('Any GA,GB WHERE GB friend GA, GA name "A", GB name "B"')) + self.assertTrue(req.execute('Any GA,U1 WHERE GA friend U1, GA name "A", U1 login "1"')) + self.assertTrue(req.execute('Any GA,U1 WHERE U1 friend GA, GA name "A", U1 login "1"')) + self.assertFalse(req.execute('Any GA,U WHERE GA friend U, GA name "A", U login "2"')) + for l1, l2 in ((u'1', u'3'), + (u'3', u'1')): + self.assertFalse(req.execute('Any U1,U2 WHERE U1 friend U2, U1 login %(l1)s, U2 login %(l2)s', + {'l1': l1, 'l2': l2})) + self.assertFalse(req.execute('Any U1,U2 WHERE U2 friend U1, U1 login %(l1)s, U2 login %(l2)s', + {'l1': l1, 'l2': l2})) + def test_html_tidy_hook(self): req = self.request() entity = req.create_entity('Workflow', name=u'wf1', diff -r 2790fb8f7e03 -r e83cbc116352 hooks/test/unittest_syncschema.py --- a/hooks/test/unittest_syncschema.py Fri Jun 21 14:22:39 2013 +0200 +++ b/hooks/test/unittest_syncschema.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 interfaces.py --- a/interfaces.py Fri Jun 21 14:22:39 2013 +0200 +++ /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 2790fb8f7e03 -r e83cbc116352 mail.py --- a/mail.py Fri Jun 21 14:22:39 2013 +0200 +++ b/mail.py Fri Jan 10 17:12:20 2014 +0100 @@ -90,7 +90,7 @@ email = u'' if uinfo.get('name'): name = uinfo['name'] - elif config and config['sender-addr']: + elif config and config['sender-name']: name = unicode(config['sender-name']) else: name = u'' diff -r 2790fb8f7e03 -r e83cbc116352 migration.py --- a/migration.py Fri Jun 21 14:22:39 2013 +0200 +++ b/migration.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 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 Fri Jan 10 17:12:20 2014 +0100 @@ -0,0 +1,129 @@ +driver = config.sources()['system']['db-driver'] +if not (driver == 'postgres' or driver.startswith('sqlserver')): + import sys + print >>sys.stderr, 'This migration is not supported for backends other than sqlserver or postgres (yet).' + sys.exit(1) + +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': + # boolean attributes with default=False were stored as '' + assert default in ('True', 'False', ''), repr(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 + + +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 driver == 'postgres': + sql('ALTER TABLE cw_cwattribute RENAME COLUMN new_defaultval TO cw_defaultval') +else: # sqlserver + sql("sp_rename 'cw_cwattribute.new_defaultval', 'cw_defaultval', 'COLUMN'") + + +# 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() + +for rschema in schema.relations(): + if rschema.symmetric: + subjects = set(repr(e.type) for e in rschema.subjects()) + objects = set(repr(e.type) for e in rschema.objects()) + assert subjects == objects + martians = set(str(eid) for eid, in sql('SELECT eid_to FROM %s_relation, entities WHERE eid_to = eid AND type NOT IN (%s)' % + (rschema.type, ','.join(subjects)))) + martians |= set(str(eid) for eid, in sql('SELECT eid_from FROM %s_relation, entities WHERE eid_from = eid AND type NOT IN (%s)' % + (rschema.type, ','.join(subjects)))) + if martians: + martians = ','.join(martians) + print 'deleting broken relations %s for eids %s' % (rschema.type, martians) + sql('DELETE FROM %s_relation WHERE eid_from IN (%s) OR eid_to IN (%s)' % (rschema.type, martians, martians)) + with session.deny_all_hooks_but(): + rql('SET X %(r)s Y WHERE Y %(r)s X, NOT X %(r)s Y' % {'r': rschema.type}) + commit() + + +# multi columns unique constraints regeneration +from cubicweb.server import schemaserial + +# syncschema hooks would try to remove indices but +# 1) we already do that below +# 2) the hook expects the CWUniqueTogetherConstraint.name attribute that hasn't +# yet been added +with session.allow_all_hooks_but('syncschema'): + rql('DELETE CWUniqueTogetherConstraint C') +commit() + +add_attribute('CWUniqueTogetherConstraint', 'name') + +# low-level wipe code for postgres & sqlserver, plain sql ... +if driver == 'postgres': + for indexname, in sql('select indexname from pg_indexes'): + if indexname.startswith('unique_'): + print 'dropping index', indexname + sql('DROP INDEX %s' % indexname) + commit() +elif driver.startswith('sqlserver'): + for viewname, in sql('select name from sys.views'): + if viewname.startswith('utv_'): + print 'dropping view (index should be cascade-deleted)', viewname + sql('DROP VIEW %s' % viewname) + commit() + +# recreate the constraints, hook will lead to low-level recreation +for eschema in sorted(schema.entities()): + if eschema._unique_together: + rql_args = schemaserial.uniquetogether2rqls(eschema) + for rql, args in rql_args: + args['x'] = eschema.eid + session.execute(rql, args) + commit() + + +add_relation_definition('CWAttribute', 'add_permission', 'CWGroup') +add_relation_definition('CWAttribute', 'add_permission', 'RQLExpression') + +# all attributes perms have to be refreshed ... +for rschema in schema.relations(): + if relation.final: + sync_schema_props_perms(rschema.type, syncprops=False) diff -r 2790fb8f7e03 -r e83cbc116352 mixins.py --- a/mixins.py Fri Jun 21 14:22:39 2013 +0200 +++ /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 2790fb8f7e03 -r e83cbc116352 predicates.py --- a/predicates.py Fri Jun 21 14:22:39 2013 +0200 +++ b/predicates.py Fri Jan 10 17:12:20 2014 +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 = {} @@ -1282,12 +1214,9 @@ ','.join(str(s) for s in self.expected)) -def on_fire_transition(etype, tr_name, from_state_name=None): +def on_fire_transition(etype, tr_names, from_state_name=None): """Return 1 when entity of the type `etype` is going through transition of - the name `tr_name`. - - If `from_state_name` is specified, this predicate will also check the - incoming state. + a name included in `tr_names`. You should use this predicate on 'after_add_entity' hook, since it's actually looking for addition of `TrInfo` entities. Hence in the hook, `self.entity` @@ -1297,9 +1226,13 @@ See :class:`cubicweb.entities.wfobjs.TrInfo` for more information. """ + if from_state_name is not None: + warn("on_fire_transition's from_state_name argument is unused", DeprecationWarning) + if isinstance(tr_names, basestring): + tr_names = set((tr_names,)) def match_etype_and_transition(trinfo): # take care trinfo.transition is None when calling change_state - return (trinfo.transition and trinfo.transition.name == tr_name + return (trinfo.transition and trinfo.transition.name in tr_names # is_instance() first two arguments are 'cls' (unused, so giving # None is fine) and the request/session and is_instance(etype)(None, trinfo._cw, entity=trinfo.for_entity)) diff -r 2790fb8f7e03 -r e83cbc116352 req.py --- a/req.py Fri Jun 21 14:22:39 2013 +0200 +++ b/req.py Fri Jan 10 17:12:20 2014 +0100 @@ -29,7 +29,10 @@ from logilab.common.deprecation import deprecated from logilab.common.date import ustrftime, strptime, todate, todatetime -from cubicweb import Unauthorized, NoSelectableObject, uilib +from rql.utils import rqlvar_maker + +from cubicweb import (Unauthorized, NoSelectableObject, NoResultError, + MultipleResultsError, uilib) from cubicweb.rset import ResultSet ONESECOND = timedelta(0, 1, 0) @@ -152,16 +155,16 @@ cls = self.vreg['etypes'].etype_class(etype) return cls.cw_instantiate(self.execute, **kwargs) + @deprecated('[3.18] use find(etype, **kwargs).entities()') def find_entities(self, etype, **kwargs): """find entities of the given type and attribute values. >>> users = find_entities('CWGroup', name=u'users') >>> groups = find_entities('CWGroup') """ - parts = ['Any X WHERE X is %s' % etype] - parts.extend('X %(attr)s %%(%(attr)s)s' % {'attr': attr} for attr in kwargs) - return self.execute(', '.join(parts), kwargs).entities() + return self.find(etype, **kwargs).entities() + @deprecated('[3.18] use find(etype, **kwargs).one()') def find_one_entity(self, etype, **kwargs): """find one entity of the given type and attribute values. raise :exc:`FindEntityError` if can not return one and only one entity. @@ -170,14 +173,43 @@ >>> groups = find_one_entity('CWGroup') Exception() """ + try: + return self.find(etype, **kwargs).one() + except (NoResultError, MultipleResultsError) as e: + raise FindEntityError("%s: (%s, %s)" % (str(e), etype, kwargs)) + + def find(self, etype, **kwargs): + """find entities of the given type and attribute values. + + :returns: A :class:`ResultSet` + + >>> users = find('CWGroup', name=u"users").one() + >>> groups = find('CWGroup').entities() + """ parts = ['Any X WHERE X is %s' % etype] - parts.extend('X %(attr)s %%(%(attr)s)s' % {'attr': attr} for attr in kwargs) + varmaker = rqlvar_maker(defined='X') + eschema = self.vreg.schema[etype] + for attr, value in kwargs.items(): + if isinstance(value, list) or isinstance(value, tuple): + raise NotImplementedError("List of values are not supported") + if hasattr(value, 'eid'): + kwargs[attr] = value.eid + if attr.startswith('reverse_'): + attr = attr[8:] + assert attr in eschema.objrels, \ + '%s not in %s object relations' % (attr, eschema) + parts.append( + '%(varname)s %(attr)s X, ' + '%(varname)s eid %%(reverse_%(attr)s)s' + % {'attr': attr, 'varname': varmaker.next()}) + else: + assert attr in eschema.subjrels, \ + '%s not in %s subject relations' % (attr, eschema) + parts.append('X %(attr)s %%(%(attr)s)s' % {'attr': attr}) + rql = ', '.join(parts) - rset = self.execute(rql, kwargs) - if len(rset) != 1: - raise FindEntityError('Found %i entitie(s) when 1 was expected (rql=%s ; %s)' - % (len(rset), rql, repr(kwargs))) - return rset.get_entity(0,0) + + return self.execute(rql, kwargs) def ensure_ro_rql(self, rql): """raise an exception if the given rql is not a select query""" @@ -424,7 +456,7 @@ """return the root url of the instance """ if secure: - return self.vreg.config.get('https-url', self.vreg.config['base-url']) + return self.vreg.config.get('https-url') or self.vreg.config['base-url'] return self.vreg.config['base-url'] # abstract methods to override according to the web front-end ############# diff -r 2790fb8f7e03 -r e83cbc116352 rqlrewrite.py --- a/rqlrewrite.py Fri Jun 21 14:22:39 2013 +0200 +++ b/rqlrewrite.py Fri Jan 10 17:12:20 2014 +0100 @@ -92,6 +92,7 @@ for etype in possibletypes: node.append(n.Constant(etype, 'etype')) else: + etype = iter(possibletypes).next() node = n.Constant(etype, 'etype') comp = mytyperel.children[1] comp.replace(comp.children[0], node) diff -r 2790fb8f7e03 -r e83cbc116352 rset.py --- a/rset.py Fri Jun 21 14:22:39 2013 +0200 +++ b/rset.py Fri Jan 10 17:12:20 2014 +0100 @@ -23,7 +23,7 @@ from rql import nodes, stmts -from cubicweb import NotAnEntity +from cubicweb import NotAnEntity, NoResultError, MultipleResultsError class ResultSet(object): @@ -437,6 +437,25 @@ raise NotAnEntity(etype) return self._build_entity(row, col) + def one(self, col=0): + """Retrieve exactly one entity from the query. + + If the result set is empty, raises :exc:`NoResultError`. + If the result set has more than one row, raises + :exc:`MultipleResultsError`. + + :type col: int + :param col: The column localising the entity in the unique row + + :return: the partially initialized `Entity` instance + """ + if len(self) == 1: + return self.get_entity(0, col) + elif len(self) == 0: + raise NoResultError("No row was found for one()") + else: + raise MultipleResultsError("Multiple rows were found for one()") + def _build_entity(self, row, col): """internal method to get a single entity, returns a partially initialized Entity instance. diff -r 2790fb8f7e03 -r e83cbc116352 schema.py --- a/schema.py Fri Jun 21 14:22:39 2013 +0200 +++ b/schema.py Fri Jan 10 17:12:20 2014 +0100 @@ -21,10 +21,11 @@ _ = unicode import re -from os.path import join +from os.path import join, basename from logging import getLogger from warnings import warn +from logilab.common import tempattr from logilab.common.decorators import cached, clear_cache, monkeypatch, cachedproperty from logilab.common.logging_ext import set_log_methods from logilab.common.deprecation import deprecated, class_moved, moved @@ -44,6 +45,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',)) @@ -94,6 +104,309 @@ ybo.ETYPE_PROPERTIES += ('eid',) ybo.RTYPE_PROPERTIES += ('eid',) +# Bases for manipulating RQL in schema ######################################### + +def guess_rrqlexpr_mainvars(expression): + defined = set(split_expression(expression)) + mainvars = set() + if 'S' in defined: + mainvars.add('S') + if 'O' in defined: + mainvars.add('O') + if 'U' in defined: + mainvars.add('U') + if not mainvars: + raise Exception('unable to guess selection variables') + return mainvars + +def split_expression(rqlstring): + for expr in rqlstring.split(','): + for noparen1 in expr.split('('): + for noparen2 in noparen1.split(')'): + for word in noparen2.split(): + yield word + +def normalize_expression(rqlstring): + """normalize an rql expression to ease schema synchronization (avoid + suppressing and reinserting an expression if only a space has been + added/removed for instance) + """ + return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(',')) + + +class RQLExpression(object): + """Base class for RQL expression used in schema (constraints and + permissions) + """ + # 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 + # to be defined in concrete classes + rqlst = None + predefined_variables = None + + def __init__(self, expression, mainvars, eid): + """ + :type mainvars: sequence of RQL variables' names. Can be provided as a + comma separated string. + :param mainvars: names of the variables being selected. + + """ + self.eid = eid # eid of the entity representing this rql expression + assert mainvars, 'bad mainvars %s' % mainvars + if isinstance(mainvars, basestring): + mainvars = set(splitstrip(mainvars)) + elif not isinstance(mainvars, set): + mainvars = set(mainvars) + self.mainvars = mainvars + self.expression = normalize_expression(expression) + try: + self.full_rql = self.rqlst.as_string() + except RQLSyntaxError: + raise RQLSyntaxError(expression) + for mainvar in mainvars: + # if variable is predefined, an extra reference is inserted + # automatically (`VAR eid %(v)s`) + if mainvar in self.predefined_variables: + min_refs = 3 + else: + min_refs = 2 + if len(self.rqlst.defined_vars[mainvar].references()) < min_refs: + _LOGGER.warn('You did not use the %s variable in your RQL ' + 'expression %s', mainvar, self) + # syntax tree used by read security (inserted in queries when necessary) + self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0] + # graph of links between variables, used by rql rewriter + self.vargraph = vargraph(self.rqlst) + # useful for some instrumentation, e.g. localperms permcheck command + self.package = ybo.PACKAGE + + def __str__(self): + return self.full_rql + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, self.full_rql) + + def __lt__(self, other): + if hasattr(other, 'expression'): + return self.expression < other.expression + return True + + def __eq__(self, other): + if hasattr(other, 'expression'): + return self.expression == other.expression + return False + + def __hash__(self): + return hash(self.expression) + + def __deepcopy__(self, memo): + return self.__class__(self.expression, self.mainvars) + def __getstate__(self): + return (self.expression, self.mainvars) + def __setstate__(self, state): + self.__init__(*state) + + @cachedproperty + def rqlst(self): + select = parse(self.minimal_rql, print_errors=False).children[0] + defined = set(split_expression(self.expression)) + for varname in self.predefined_variables: + if varname in defined: + select.add_eid_restriction(select.get_variable(varname), varname.lower(), 'Substitute') + return select + + # permission rql expression specific stuff ################################# + + @cached + def transform_has_permission(self): + found = None + rqlst = self.rqlst + for var in rqlst.defined_vars.itervalues(): + for varref in var.references(): + rel = varref.relation() + if rel is None: + continue + try: + prefix, action, suffix = rel.r_type.split('_') + except ValueError: + continue + if prefix != 'has' or suffix != 'permission' or \ + not action in ('add', 'delete', 'update', 'read'): + continue + if found is None: + found = [] + rqlst.save_state() + assert rel.children[0].name == 'U' + objvar = rel.children[1].children[0].variable + rqlst.remove_node(rel) + selected = [v.name for v in rqlst.get_selected_variables()] + if objvar.name not in selected: + colindex = len(selected) + rqlst.add_selected(objvar) + else: + colindex = selected.index(objvar.name) + found.append((action, colindex)) + # remove U eid %(u)s if U is not used in any other relation + uvrefs = rqlst.defined_vars['U'].references() + if len(uvrefs) == 1: + rqlst.remove_node(uvrefs[0].relation()) + if found is not None: + rql = rqlst.as_string() + if len(rqlst.selection) == 1 and isinstance(rqlst.where, nodes.Relation): + # only "Any X WHERE X eid %(x)s" remaining, no need to execute the rql + keyarg = rqlst.selection[0].name.lower() + else: + keyarg = None + rqlst.recover() + return rql, found, keyarg + return rqlst.as_string(), None, None + + def _check(self, _cw, **kwargs): + """return True if the rql expression is matching the given relation + between fromeid and toeid + + _cw may be a request or a server side transaction + """ + creating = kwargs.get('creating') + if not creating and self.eid is not None: + key = (self.eid, tuple(sorted(kwargs.iteritems()))) + try: + return _cw.local_perm_cache[key] + except KeyError: + pass + rql, has_perm_defs, keyarg = self.transform_has_permission() + # when creating an entity, expression related to X satisfied + if creating and 'X' in self.rqlst.defined_vars: + return True + if keyarg is None: + kwargs.setdefault('u', _cw.user.eid) + try: + rset = _cw.execute(rql, kwargs, build_descr=True) + except NotImplementedError: + self.critical('cant check rql expression, unsupported rql %s', rql) + if self.eid is not None: + _cw.local_perm_cache[key] = False + return False + except TypeResolverException as ex: + # some expression may not be resolvable with current kwargs + # (type conflict) + self.warning('%s: %s', rql, str(ex)) + if self.eid is not None: + _cw.local_perm_cache[key] = False + return False + except Unauthorized as ex: + self.debug('unauthorized %s: %s', rql, str(ex)) + if self.eid is not None: + _cw.local_perm_cache[key] = False + return False + else: + rset = _cw.eid_rset(kwargs[keyarg]) + # if no special has_*_permission relation in the rql expression, just + # check the result set contains something + if has_perm_defs is None: + if rset: + if self.eid is not None: + _cw.local_perm_cache[key] = True + return True + elif rset: + # check every special has_*_permission relation is satisfied + get_eschema = _cw.vreg.schema.eschema + try: + for eaction, col in has_perm_defs: + for i in xrange(len(rset)): + eschema = get_eschema(rset.description[i][col]) + eschema.check_perm(_cw, eaction, eid=rset[i][col]) + if self.eid is not None: + _cw.local_perm_cache[key] = True + return True + except Unauthorized: + pass + if self.eid is not None: + _cw.local_perm_cache[key] = False + return False + + @property + def minimal_rql(self): + return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)), + self.expression) + + + +# rql expressions for use in permission definition ############################# + +class ERQLExpression(RQLExpression): + predefined_variables = 'XU' + + def __init__(self, expression, mainvars=None, eid=None): + RQLExpression.__init__(self, expression, mainvars or 'X', eid) + + def check(self, _cw, eid=None, creating=False, **kwargs): + if 'X' in self.rqlst.defined_vars: + if eid is None: + if creating: + return self._check(_cw, creating=True, **kwargs) + return False + assert creating == False + return self._check(_cw, x=eid, **kwargs) + return self._check(_cw, **kwargs) + + +def vargraph(rqlst): + """ builds an adjacency graph of variables from the rql syntax tree, e.g: + Any O,S WHERE T subworkflow_exit S, T subworkflow WF, O state_of WF + => {'WF': ['O', 'T'], 'S': ['T'], 'T': ['WF', 'S'], 'O': ['WF']} + """ + vargraph = {} + for relation in rqlst.get_nodes(nodes.Relation): + try: + rhsvarname = relation.children[1].children[0].variable.name + lhsvarname = relation.children[0].name + except AttributeError: + pass + else: + vargraph.setdefault(lhsvarname, []).append(rhsvarname) + vargraph.setdefault(rhsvarname, []).append(lhsvarname) + #vargraph[(lhsvarname, rhsvarname)] = relation.r_type + return vargraph + + +class GeneratedConstraint(object): + def __init__(self, rqlst, mainvars): + self.snippet_rqlst = rqlst + self.mainvars = mainvars + self.vargraph = vargraph(rqlst) + + +class RRQLExpression(RQLExpression): + predefined_variables = 'SOU' + + def __init__(self, expression, mainvars=None, eid=None): + if mainvars is None: + mainvars = guess_rrqlexpr_mainvars(expression) + RQLExpression.__init__(self, expression, mainvars, eid) + + def check(self, _cw, fromeid=None, toeid=None): + kwargs = {} + if 'S' in self.rqlst.defined_vars: + if fromeid is None: + return False + kwargs['s'] = fromeid + if 'O' in self.rqlst.defined_vars: + if toeid is None: + return False + kwargs['o'] = toeid + return self._check(_cw, **kwargs) + + +# In yams, default 'update' perm for attributes granted to managers and owners. +# Within cw, we want to default to users who may edit the entity holding the +# attribute. +# These default permissions won't be checked by the security hooks: +# since they delegate checking to the entity, we can skip actual checks. +ybo.DEFAULT_ATTRPERMS['update'] = ('managers', ERQLExpression('U has_update_permission X')) +ybo.DEFAULT_ATTRPERMS['add'] = ('managers', ERQLExpression('U has_add_permission X')) + + PUB_SYSTEM_ENTITY_PERMS = { 'read': ('managers', 'users', 'guests',), 'add': ('managers',), @@ -107,6 +420,7 @@ } PUB_SYSTEM_ATTR_PERMS = { 'read': ('managers', 'users', 'guests',), + 'add': ('managers',), 'update': ('managers',), } RO_REL_PERMS = { @@ -116,6 +430,7 @@ } RO_ATTR_PERMS = { 'read': ('managers', 'users', 'guests',), + 'add': ybo.DEFAULT_ATTRPERMS['add'], 'update': (), } @@ -268,13 +583,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 +611,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 @@ -630,301 +964,6 @@ def schema_by_eid(self, eid): return self._eid_index[eid] -# Bases for manipulating RQL in schema ######################################### - -def guess_rrqlexpr_mainvars(expression): - defined = set(split_expression(expression)) - mainvars = set() - if 'S' in defined: - mainvars.add('S') - if 'O' in defined: - mainvars.add('O') - if 'U' in defined: - mainvars.add('U') - if not mainvars: - raise Exception('unable to guess selection variables') - return mainvars - -def split_expression(rqlstring): - for expr in rqlstring.split(','): - for noparen1 in expr.split('('): - for noparen2 in noparen1.split(')'): - for word in noparen2.split(): - yield word - -def normalize_expression(rqlstring): - """normalize an rql expression to ease schema synchronization (avoid - suppressing and reinserting an expression if only a space has been - added/removed for instance) - """ - return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(',')) - - -class RQLExpression(object): - """Base class for RQL expression used in schema (constraints and - permissions) - """ - # 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 - # to be defined in concrete classes - rqlst = None - predefined_variables = None - - def __init__(self, expression, mainvars, eid): - """ - :type mainvars: sequence of RQL variables' names. Can be provided as a - comma separated string. - :param mainvars: names of the variables being selected. - - """ - self.eid = eid # eid of the entity representing this rql expression - assert mainvars, 'bad mainvars %s' % mainvars - if isinstance(mainvars, basestring): - mainvars = set(splitstrip(mainvars)) - elif not isinstance(mainvars, set): - mainvars = set(mainvars) - self.mainvars = mainvars - self.expression = normalize_expression(expression) - try: - self.full_rql = self.rqlst.as_string() - except RQLSyntaxError: - raise RQLSyntaxError(expression) - for mainvar in mainvars: - # if variable is predefined, an extra reference is inserted - # automatically (`VAR eid %(v)s`) - if mainvar in self.predefined_variables: - min_refs = 3 - else: - min_refs = 2 - if len(self.rqlst.defined_vars[mainvar].references()) < min_refs: - _LOGGER.warn('You did not use the %s variable in your RQL ' - 'expression %s', mainvar, self) - # syntax tree used by read security (inserted in queries when necessary) - self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0] - # graph of links between variables, used by rql rewriter - self.vargraph = vargraph(self.rqlst) - - def __str__(self): - return self.full_rql - def __repr__(self): - return '%s(%s)' % (self.__class__.__name__, self.full_rql) - - def __lt__(self, other): - if hasattr(other, 'expression'): - return self.expression < other.expression - return True - - def __eq__(self, other): - if hasattr(other, 'expression'): - return self.expression == other.expression - return False - - def __hash__(self): - return hash(self.expression) - - def __deepcopy__(self, memo): - return self.__class__(self.expression, self.mainvars) - def __getstate__(self): - return (self.expression, self.mainvars) - def __setstate__(self, state): - self.__init__(*state) - - @cachedproperty - def rqlst(self): - select = parse(self.minimal_rql, print_errors=False).children[0] - defined = set(split_expression(self.expression)) - for varname in self.predefined_variables: - if varname in defined: - select.add_eid_restriction(select.get_variable(varname), varname.lower(), 'Substitute') - return select - - # permission rql expression specific stuff ################################# - - @cached - def transform_has_permission(self): - found = None - rqlst = self.rqlst - for var in rqlst.defined_vars.itervalues(): - for varref in var.references(): - rel = varref.relation() - if rel is None: - continue - try: - prefix, action, suffix = rel.r_type.split('_') - except ValueError: - continue - if prefix != 'has' or suffix != 'permission' or \ - not action in ('add', 'delete', 'update', 'read'): - continue - if found is None: - found = [] - rqlst.save_state() - assert rel.children[0].name == 'U' - objvar = rel.children[1].children[0].variable - rqlst.remove_node(rel) - selected = [v.name for v in rqlst.get_selected_variables()] - if objvar.name not in selected: - colindex = len(selected) - rqlst.add_selected(objvar) - else: - colindex = selected.index(objvar.name) - found.append((action, colindex)) - # remove U eid %(u)s if U is not used in any other relation - uvrefs = rqlst.defined_vars['U'].references() - if len(uvrefs) == 1: - rqlst.remove_node(uvrefs[0].relation()) - if found is not None: - rql = rqlst.as_string() - if len(rqlst.selection) == 1 and isinstance(rqlst.where, nodes.Relation): - # only "Any X WHERE X eid %(x)s" remaining, no need to execute the rql - keyarg = rqlst.selection[0].name.lower() - else: - keyarg = None - rqlst.recover() - return rql, found, keyarg - return rqlst.as_string(), None, None - - def _check(self, _cw, **kwargs): - """return True if the rql expression is matching the given relation - between fromeid and toeid - - _cw may be a request or a server side transaction - """ - creating = kwargs.get('creating') - if not creating and self.eid is not None: - key = (self.eid, tuple(sorted(kwargs.iteritems()))) - try: - return _cw.local_perm_cache[key] - except KeyError: - pass - rql, has_perm_defs, keyarg = self.transform_has_permission() - # when creating an entity, expression related to X satisfied - if creating and 'X' in self.rqlst.defined_vars: - return True - if keyarg is None: - kwargs.setdefault('u', _cw.user.eid) - try: - rset = _cw.execute(rql, kwargs, build_descr=True) - except NotImplementedError: - self.critical('cant check rql expression, unsupported rql %s', rql) - if self.eid is not None: - _cw.local_perm_cache[key] = False - return False - except TypeResolverException as ex: - # some expression may not be resolvable with current kwargs - # (type conflict) - self.warning('%s: %s', rql, str(ex)) - if self.eid is not None: - _cw.local_perm_cache[key] = False - return False - except Unauthorized as ex: - self.debug('unauthorized %s: %s', rql, str(ex)) - if self.eid is not None: - _cw.local_perm_cache[key] = False - return False - else: - rset = _cw.eid_rset(kwargs[keyarg]) - # if no special has_*_permission relation in the rql expression, just - # check the result set contains something - if has_perm_defs is None: - if rset: - if self.eid is not None: - _cw.local_perm_cache[key] = True - return True - elif rset: - # check every special has_*_permission relation is satisfied - get_eschema = _cw.vreg.schema.eschema - try: - for eaction, col in has_perm_defs: - for i in xrange(len(rset)): - eschema = get_eschema(rset.description[i][col]) - eschema.check_perm(_cw, eaction, eid=rset[i][col]) - if self.eid is not None: - _cw.local_perm_cache[key] = True - return True - except Unauthorized: - pass - if self.eid is not None: - _cw.local_perm_cache[key] = False - return False - - @property - def minimal_rql(self): - return 'Any %s WHERE %s' % (','.join(sorted(self.mainvars)), - self.expression) - -# rql expressions for use in permission definition ############################# - -class ERQLExpression(RQLExpression): - predefined_variables = 'XU' - - def __init__(self, expression, mainvars=None, eid=None): - RQLExpression.__init__(self, expression, mainvars or 'X', eid) - - def check(self, _cw, eid=None, creating=False, **kwargs): - if 'X' in self.rqlst.defined_vars: - if eid is None: - if creating: - return self._check(_cw, creating=True, **kwargs) - return False - assert creating == False - return self._check(_cw, x=eid, **kwargs) - return self._check(_cw, **kwargs) - - -def vargraph(rqlst): - """ builds an adjacency graph of variables from the rql syntax tree, e.g: - Any O,S WHERE T subworkflow_exit S, T subworkflow WF, O state_of WF - => {'WF': ['O', 'T'], 'S': ['T'], 'T': ['WF', 'S'], 'O': ['WF']} - """ - vargraph = {} - for relation in rqlst.get_nodes(nodes.Relation): - try: - rhsvarname = relation.children[1].children[0].variable.name - lhsvarname = relation.children[0].name - except AttributeError: - pass - else: - vargraph.setdefault(lhsvarname, []).append(rhsvarname) - vargraph.setdefault(rhsvarname, []).append(lhsvarname) - #vargraph[(lhsvarname, rhsvarname)] = relation.r_type - return vargraph - - -class GeneratedConstraint(object): - def __init__(self, rqlst, mainvars): - self.snippet_rqlst = rqlst - self.mainvars = mainvars - self.vargraph = vargraph(rqlst) - - -class RRQLExpression(RQLExpression): - predefined_variables = 'SOU' - - def __init__(self, expression, mainvars=None, eid=None): - if mainvars is None: - mainvars = guess_rrqlexpr_mainvars(expression) - RQLExpression.__init__(self, expression, mainvars, eid) - - def check(self, _cw, fromeid=None, toeid=None): - kwargs = {} - if 'S' in self.rqlst.defined_vars: - if fromeid is None: - return False - kwargs['s'] = fromeid - if 'O' in self.rqlst.defined_vars: - if toeid is None: - return False - kwargs['o'] = toeid - return self._check(_cw, **kwargs) - - -# in yams, default 'update' perm for attributes granted to managers and owners. -# Within cw, we want to default to users who may edit the entity holding the -# attribute. -ybo.DEFAULT_ATTRPERMS['update'] = ( - 'managers', ERQLExpression('U has_update_permission X')) # additional cw specific constraints ########################################### @@ -933,14 +972,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 +1031,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) @@ -1141,7 +1174,8 @@ # bootstraping, ignore cubes filepath = join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'bootstrap.py') self.info('loading %s', filepath) - self.handle_file(filepath) + with tempattr(ybo, 'PACKAGE', 'cubicweb'): # though we don't care here + self.handle_file(filepath) def unhandled_file(self, filepath): """called when a file without handler associated has been found""" @@ -1181,11 +1215,12 @@ join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'workflow.py'), join(cubicweb.CW_SOFTWARE_ROOT, 'schemas', 'Bookmark.py')): self.info('loading %s', filepath) - self.handle_file(filepath) + with tempattr(ybo, 'PACKAGE', 'cubicweb'): + self.handle_file(filepath) for cube in cubes: for filepath in self.get_schema_files(cube): - self.info('loading %s', filepath) - self.handle_file(filepath) + with tempattr(ybo, 'PACKAGE', basename(cube)): + self.handle_file(filepath) # these are overridden by set_log_methods below # only defining here to prevent pylint from complaining diff -r 2790fb8f7e03 -r e83cbc116352 schemas/_regproc.postgres.sql --- a/schemas/_regproc.postgres.sql Fri Jun 21 14:22:39 2013 +0200 +++ b/schemas/_regproc.postgres.sql Fri Jan 10 17:12:20 2014 +0100 @@ -10,10 +10,15 @@ SELECT array_to_string($1, ', ') $$ LANGUAGE SQL;; + +CREATE FUNCTION cw_array_append_unique (anyarray, anyelement) RETURNS anyarray AS $$ + SELECT array_append($1, (SELECT $2 WHERE $2 <> ALL($1))) +$$ LANGUAGE SQL + DROP AGGREGATE IF EXISTS group_concat (anyelement) CASCADE; CREATE AGGREGATE group_concat ( basetype = anyelement, - sfunc = array_append, + sfunc = cw_array_append_unique, stype = anyarray, finalfunc = comma_join, initcond = '{}' diff -r 2790fb8f7e03 -r e83cbc116352 schemas/bootstrap.py --- a/schemas/bootstrap.py Fri Jun 21 14:22:39 2013 +0200 +++ b/schemas/bootstrap.py Fri Jan 10 17:12:20 2014 +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, @@ -158,6 +158,7 @@ class CWUniqueTogetherConstraint(EntityType): """defines a sql-level multicolumn unique index""" __permissions__ = PUB_SYSTEM_ENTITY_PERMS + name = String(required=True, unique=True, maxsize=64) constraint_of = SubjectRelation('CWEType', cardinality='1*', composite='object', inlined=True) relations = SubjectRelation('CWRType', cardinality='+*', @@ -235,7 +236,7 @@ """groups allowed to add entities/relations of this type""" __permissions__ = PUB_SYSTEM_REL_PERMS name = 'add_permission' - subject = ('CWEType', 'CWRelation') + subject = ('CWEType', 'CWRelation', 'CWAttribute') object = 'CWGroup' cardinality = '**' @@ -268,7 +269,7 @@ """rql expression allowing to add entities/relations of this type""" __permissions__ = PUB_SYSTEM_REL_PERMS name = 'add_permission' - subject = ('CWEType', 'CWRelation') + subject = ('CWEType', 'CWRelation', 'CWAttribute') object = 'RQLExpression' cardinality = '*?' composite = 'subject' diff -r 2790fb8f7e03 -r e83cbc116352 server/__init__.py --- a/server/__init__.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/__init__.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 server/hook.py --- a/server/hook.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/hook.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 server/hooksmanager.py --- a/server/hooksmanager.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/hooksmanager.py Fri Jan 10 17:12:20 2014 +0100 @@ -17,11 +17,6 @@ # with CubicWeb. If not, see . from logilab.common.deprecation import class_renamed, class_moved from cubicweb.server import hook + SystemHook = class_renamed('SystemHook', hook.Hook) -PropagateSubjectRelationHook = class_renamed('PropagateSubjectRelationHook', - hook.PropagateSubjectRelationHook) -PropagateSubjectRelationAddHook = class_renamed('PropagateSubjectRelationAddHook', - hook.PropagateSubjectRelationAddHook) -PropagateSubjectRelationDelHook = class_renamed('PropagateSubjectRelationDelHook', - hook.PropagateSubjectRelationDelHook) Hook = class_moved(hook.Hook) diff -r 2790fb8f7e03 -r e83cbc116352 server/migractions.py --- a/server/migractions.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/migractions.py Fri Jan 10 17:12:20 2014 +0100 @@ -44,7 +44,7 @@ from logilab.common.decorators import cached, clear_cache from yams.constraints import SizeConstraint -from yams.schema2sql import eschema2sql, rschema2sql +from yams.schema2sql import eschema2sql, rschema2sql, unique_index_name from yams.schema import RelationDefinitionSchema from cubicweb import CW_SOFTWARE_ROOT, AuthenticationError, ExecutionError @@ -559,39 +559,41 @@ self._synchronize_rdef_schema(subj, rschema, obj, syncprops=syncprops, syncperms=syncperms) if syncprops: # need to process __unique_together__ after rdefs were processed - repo_unique_together = set([frozenset(ut) - for ut in repoeschema._unique_together]) - unique_together = set([frozenset(ut) - for ut in eschema._unique_together]) - for ut in repo_unique_together - unique_together: - restrictions = [] - substs = {'x': repoeschema.eid} - for i, col in enumerate(ut): - restrictions.append('C relations T%(i)d, ' - 'T%(i)d name %%(T%(i)d)s' % {'i': i}) - substs['T%d'%i] = col - self.rqlexec('DELETE CWUniqueTogetherConstraint C ' - 'WHERE C constraint_of E, ' - ' E eid %%(x)s,' - ' %s' % ', '.join(restrictions), - substs) - def possible_unique_constraint(ut): - for name in ut: + # mappings from constraint name to columns + # filesystem (fs) and repository (repo) wise + fs = {} + repo = {} + for cols in eschema._unique_together or (): + fs[unique_index_name(repoeschema, cols)] = sorted(cols) + schemaentity = self.session.entity_from_eid(repoeschema.eid) + for entity in schemaentity.related('constraint_of', 'object', + targettypes=('CWUniqueTogetherConstraint',)).entities(): + repo[entity.name] = sorted(rel.name for rel in entity.relations) + added = set(fs) - set(repo) + removed = set(repo) - set(fs) + + for name in removed: + self.rqlexec('DELETE CWUniqueTogetherConstraint C WHERE C name %(name)s', + {'name': name}) + + def possible_unique_constraint(cols): + for name in cols: rschema = repoeschema.subjrels.get(name) if rschema is None: print 'dont add %s unique constraint on %s, missing %s' % ( - ','.join(ut), eschema, name) + ','.join(cols), eschema, name) return False if not (rschema.final or rschema.inlined): - (eschema, name) print 'dont add %s unique constraint on %s, %s is neither final nor inlined' % ( - ','.join(ut), eschema, name) + ','.join(cols), eschema, name) return False return True - for ut in unique_together - repo_unique_together: - if possible_unique_constraint(ut): - rql, substs = ss.uniquetogether2rql(eschema, ut) + + for name in added: + if possible_unique_constraint(fs[name]): + rql, substs = ss._uniquetogether2rql(eschema, fs[name]) substs['x'] = repoeschema.eid + substs['name'] = name self.rqlexec(rql, substs) def _synchronize_rdef_schema(self, subjtype, rtype, objtype, @@ -1461,12 +1463,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 2790fb8f7e03 -r e83cbc116352 server/msplanner.py --- a/server/msplanner.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/msplanner.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 server/querier.py --- a/server/querier.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/querier.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 server/repository.py --- a/server/repository.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/repository.py Fri Jan 10 17:12:20 2014 +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: @@ -1592,10 +1595,6 @@ source.delete_relation(session, subject, rtype, object) rschema = self.schema.rschema(rtype) session.update_rel_cache_del(subject, rtype, object, rschema.symmetric) - if rschema.symmetric: - # on symmetric relation, we can't now in which sense it's - # stored so try to delete both - source.delete_relation(session, object, rtype, subject) if source.should_call_hooks: self.hm.call_hooks('after_delete_relation', session, eidfrom=subject, rtype=rtype, eidto=object) @@ -1665,18 +1664,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 2790fb8f7e03 -r e83cbc116352 server/rqlannotation.py --- a/server/rqlannotation.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/rqlannotation.py Fri Jan 10 17:12:20 2014 +0100 @@ -130,8 +130,6 @@ # can use N.ecrit_par as principal if (stinfo['selected'] or len(stinfo['relations']) > 1): break - elif rschema.symmetric and stinfo['selected']: - break joins.add( (rel, role) ) else: # if there is at least one ambigous relation and no other to diff -r 2790fb8f7e03 -r e83cbc116352 server/schemaserial.py --- a/server/schemaserial.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/schemaserial.py Fri Jan 10 17:12:20 2014 +0100 @@ -25,9 +25,10 @@ from logilab.common.shellutils import ProgressBar -from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo +from yams import (BadSchemaDefinition, schema as schemamod, buildobjs as ybo, + schema2sql as y2sql) -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 +78,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 +213,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 +238,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 @@ -304,9 +304,6 @@ except KeyError: return for action, somethings in thispermsdict.iteritems(): - # XXX cw < 3.6.1 bw compat - if isinstance(erschema, schemamod.RelationDefinitionSchema) and erschema.final and action == 'add': - action = 'update' erschema.permissions[action] = tuple( isinstance(p, tuple) and erschema.rql_expression(*p) or p for p in somethings) @@ -344,13 +341,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 @@ -371,8 +365,8 @@ pb.update() # serialize unique_together constraints for eschema in eschemas: - for unique_together in eschema._unique_together: - execschemarql(execute, eschema, [uniquetogether2rql(eschema, unique_together)]) + if eschema._unique_together: + execschemarql(execute, eschema, uniquetogether2rqls(eschema)) # serialize yams inheritance relationships for rql, kwargs in specialize2rql(schema): execute(rql, kwargs, build_descr=False) @@ -431,7 +425,15 @@ values = {'x': eschema.eid, 'et': specialized_type.eid} yield 'SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s', values -def uniquetogether2rql(eschema, unique_together): +def uniquetogether2rqls(eschema): + rql_args = [] + for columns in eschema._unique_together: + rql, args = _uniquetogether2rql(eschema, columns) + args['name'] = y2sql.unique_index_name(eschema, columns) + rql_args.append((rql, args)) + return rql_args + +def _uniquetogether2rql(eschema, unique_together): relations = [] restrictions = [] substs = {} @@ -443,10 +445,8 @@ restrictions.append('%(rtype)s name %%(%(rtype)s)s' % {'rtype': rtype}) relations = ', '.join(relations) restrictions = ', '.join(restrictions) - rql = ('INSERT CWUniqueTogetherConstraint C: ' - ' C constraint_of X, %s ' - 'WHERE ' - ' X eid %%(x)s, %s') + rql = ('INSERT CWUniqueTogetherConstraint C: C name %%(name)s, C constraint_of X, %s ' + 'WHERE X eid %%(x)s, %s') return rql % (relations, restrictions), substs @@ -536,10 +536,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 2790fb8f7e03 -r e83cbc116352 server/serverctl.py --- a/server/serverctl.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/serverctl.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 server/session.py --- a/server/session.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/session.py Fri Jan 10 17:12:20 2014 +0100 @@ -1082,7 +1082,7 @@ def keep_cnxset_mode(self, mode): """set `mode`, e.g. how the session will keep its connections set: - * if mode == 'write', the connections set is freed after each ready + * if mode == 'write', the connections set is freed after each read query, but kept until the transaction's end (eg commit or rollback) when a write query is detected (eg INSERT/SET/DELETE queries) @@ -1174,14 +1174,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 2790fb8f7e03 -r e83cbc116352 server/sources/__init__.py --- a/server/sources/__init__.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/sources/__init__.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 server/sources/ldapuser.py --- a/server/sources/ldapuser.py Fri Jun 21 14:22:39 2013 +0200 +++ /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 2790fb8f7e03 -r e83cbc116352 server/sources/native.py --- a/server/sources/native.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/sources/native.py Fri Jan 10 17:12:20 2014 +0100 @@ -757,24 +757,17 @@ if ex.__class__.__name__ == 'IntegrityError': # need string comparison because of various backends for arg in ex.args: - if 'SQL Server' in arg: - mo = re.search("'unique_cw_[^ ]+'", arg) - else: # postgres - mo = re.search('"unique_cw_[^ ]+"', arg) + # postgres, sqlserver + mo = re.search("unique_[a-z0-9]{32}", arg) if mo is not None: - index_name = mo.group(0)[1:-1] # eat the surrounding " pair - elements = index_name.split('_cw_')[1:] - etype = elements[0] - rtypes = elements[1:] - raise UniqueTogetherError(etype, rtypes) + raise UniqueTogetherError(session, cstrname=mo.group(0)) # sqlite mo = re.search('columns (.*) are not unique', arg) if mo is not None: # sqlite in use # we left chop the 'cw_' prefix of attribute names rtypes = [c.strip()[3:] for c in mo.group(1).split(',')] - etype = '???' - raise UniqueTogetherError(etype, rtypes) + raise UniqueTogetherError(session, rtypes=rtypes) raise return cursor diff -r 2790fb8f7e03 -r e83cbc116352 server/sources/pyrorql.py --- a/server/sources/pyrorql.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/sources/pyrorql.py Fri Jan 10 17:12:20 2014 +0100 @@ -20,6 +20,11 @@ __docformat__ = "restructuredtext en" _ = unicode +# module is lazily imported +import warnings +warnings.warn('Imminent drop of pyrorql source. Switch to datafeed now!', + DeprecationWarning) + import threading from Pyro.errors import PyroError, ConnectionClosedError diff -r 2790fb8f7e03 -r e83cbc116352 server/sources/rql2sql.py --- a/server/sources/rql2sql.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/sources/rql2sql.py Fri Jan 10 17:12:20 2014 +0100 @@ -242,12 +242,6 @@ rhsconst = None # ColumnAlias return lhs, lhsconst, rhs, rhsconst -def switch_relation_field(sql, table=''): - switchedsql = sql.replace(table + '.eid_from', '__eid_from__') - switchedsql = switchedsql.replace(table + '.eid_to', - table + '.eid_from') - return switchedsql.replace('__eid_from__', table + '.eid_to') - def sort_term_selection(sorts, rqlst, groups): # XXX beurk if isinstance(rqlst, list): @@ -1132,8 +1126,6 @@ sqls += self._process_relation_term(relation, rid, lhsvar, lhsconst, 'eid_from') sqls += self._process_relation_term(relation, rid, rhsvar, rhsconst, 'eid_to') sql = ' AND '.join(sqls) - if rschema.symmetric: - sql = '(%s OR %s)' % (sql, switch_relation_field(sql)) return sql def _visit_outer_join_relation(self, relation, rschema): diff -r 2790fb8f7e03 -r e83cbc116352 server/sqlutils.py --- a/server/sqlutils.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/sqlutils.py Fri Jan 10 17:12:20 2014 +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) @@ -332,10 +327,10 @@ class group_concat(object): def __init__(self): - self.values = [] + self.values = set() def step(self, value): if value is not None: - self.values.append(value) + self.values.add(value) def finalize(self): return ', '.join(unicode(v) for v in self.values) diff -r 2790fb8f7e03 -r e83cbc116352 server/test/data/migratedapp/schema.py --- a/server/test/data/migratedapp/schema.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/test/data/migratedapp/schema.py Fri Jan 10 17:12:20 2014 +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. @@ -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) @@ -38,13 +39,27 @@ concerne = SubjectRelation('Societe') opt_attr = Bytes() -class concerne(RelationType): +class Societe(WorkflowableEntityType): __permissions__ = { - 'read': ('managers', 'users', 'guests'), - 'add': ('managers', RRQLExpression('U has_update_permission S')), - 'delete': ('managers', RRQLExpression('O owned_by U')), + 'read': ('managers', 'users', 'guests'), + 'update': ('managers', 'owners'), + 'delete': ('managers', 'owners'), + 'add': ('managers', 'users',) } + nom = String(maxsize=64, fulltextindexed=True) + web = String(maxsize=128) + tel = Int() + fax = Int() + rncs = String(maxsize=128) + ad1 = String(maxsize=128) + ad2 = String(maxsize=128) + ad3 = String(maxsize=128) + cp = String(maxsize=12) + ville= String(maxsize=32) +# Division and SubDivision are gone + +# New class Para(EntityType): para = String(maxsize=512) newattr = String() @@ -62,43 +77,18 @@ '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') -class Text(Para): - __specializes_schema__ = True - summary = String(maxsize=512) - -class ecrit_par(RelationType): - __permissions__ = {'read': ('managers', 'users', 'guests',), - 'delete': ('managers', ), - 'add': ('managers', - RRQLExpression('O require_permission P, P name "add_note", ' - 'U in_group G, P require_group G'),) - } - inlined = True - cardinality = '?*' - - -class Folder2(EntityType): - """folders are used to classify entities. They may be defined as a tree. - When you include the Folder entity, all application specific entities - may then be classified using the "filed_under" relation. - """ - name = String(required=True, indexed=True, internationalizable=True, - constraints=[UniqueConstraint(), SizeConstraint(64)]) - description = RichString(fulltextindexed=True) - -class filed_under2(RelationDefinition): - subject ='*' - object = 'Folder2' - class Personne(EntityType): __unique_together__ = [('nom', 'prenom', 'datenaiss')] @@ -120,32 +110,71 @@ concerne2 = SubjectRelation(('Affaire', 'Note'), cardinality='1*') connait = SubjectRelation('Personne', symmetric=True) +class concerne(RelationType): + __permissions__ = { + 'read': ('managers', 'users', 'guests'), + 'add': ('managers', RRQLExpression('U has_update_permission S')), + 'delete': ('managers', RRQLExpression('O owned_by U')), + } +# `Old` entity type is gonce +# `comments` is gone +# `fiche` is gone +# `multisource_*` rdefs are gone +# `see_also_*` rdefs are gone + +class evaluee(RelationDefinition): + subject = ('Personne', 'CWUser', 'Societe') + object = ('Note') + +class ecrit_par(RelationType): + __permissions__ = {'read': ('managers', 'users', 'guests',), + 'delete': ('managers', ), + 'add': ('managers', + RRQLExpression('O require_permission P, P name "add_note", ' + 'U in_group G, P require_group G'),) + } + inlined = True + cardinality = '?*' + +# `copain` rdef is gone +# `tags` rdef is gone +# `filed_under` rdef is gone +# `require_permission` rdef is gone +# `require_state` rdef is gone +# `personne_composite` rdef is gone +# `personne_inlined` rdef is gone +# `login_user` rdef is gone +# `ambiguous_inlined` rdef is gone + +# New +class Text(Para): + __specializes_schema__ = True + summary = String(maxsize=512) + + +# New +class Folder2(EntityType): + """folders are used to classify entities. They may be defined as a tree. + When you include the Folder entity, all application specific entities + may then be classified using the "filed_under" relation. + """ + name = String(required=True, indexed=True, internationalizable=True, + constraints=[UniqueConstraint(), SizeConstraint(64)]) + description = RichString(fulltextindexed=True) + +# New +class filed_under2(RelationDefinition): + subject ='*' + object = 'Folder2' + + +# New class New(EntityType): new_name = String() -class Societe(WorkflowableEntityType): - __permissions__ = { - 'read': ('managers', 'users', 'guests'), - 'update': ('managers', 'owners'), - 'delete': ('managers', 'owners'), - 'add': ('managers', 'users',) - } - nom = String(maxsize=64, fulltextindexed=True) - web = String(maxsize=128) - tel = Int() - fax = Int() - rncs = String(maxsize=128) - ad1 = String(maxsize=128) - ad2 = String(maxsize=128) - ad3 = String(maxsize=128) - cp = String(maxsize=12) - ville= String(maxsize=32) - +# New class same_as(RelationDefinition): subject = ('Societe',) object = 'ExternalUri' -class evaluee(RelationDefinition): - subject = ('Personne', 'CWUser', 'Societe') - object = ('Note') diff -r 2790fb8f7e03 -r e83cbc116352 server/test/data/schema.py --- a/server/test/data/schema.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/test/data/schema.py Fri Jan 10 17:12:20 2014 +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. @@ -94,7 +94,12 @@ 'read': ('managers', 'users', 'guests'), 'update': ('managers', ERQLExpression('X in_state S, S name "todo"')), }) - + something = String(maxsize=1, + __permissions__ = { + 'read': ('managers', 'users', 'guests'), + 'add': (ERQLExpression('NOT X para NULL'),), + 'update': ('managers', 'owners') + }) migrated_from = SubjectRelation('Note') attachment = SubjectRelation('File') inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*', @@ -119,6 +124,7 @@ tzdatenaiss = TZDatetime() test = Boolean(__permissions__={ 'read': ('managers', 'users', 'guests'), + 'add': ('managers',), 'update': ('managers',), }) description = String() diff -r 2790fb8f7e03 -r e83cbc116352 server/test/unittest_ldapsource.py --- a/server/test/unittest_ldapsource.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/test/unittest_ldapsource.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/test/unittest_migractions.py Fri Jan 10 17:12:20 2014 +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): @@ -389,8 +416,8 @@ self.assertEqual(eexpr.reverse_read_permission, ()) self.assertEqual(eexpr.reverse_delete_permission, ()) self.assertEqual(eexpr.reverse_update_permission, ()) - # no more rqlexpr to delete and add para attribute - self.assertFalse(self._rrqlexpr_rset('add', 'para')) + self.assertTrue(self._rrqlexpr_rset('add', 'para')) + # no rqlexpr to delete para attribute self.assertFalse(self._rrqlexpr_rset('delete', 'para')) # new rql expr to add ecrit_par relation rexpr = self._rrqlexpr_entity('add', 'ecrit_par') @@ -418,19 +445,24 @@ self.assertEqual(len(self._rrqlexpr_rset('delete', 'concerne')), len(delete_concerne_rqlexpr)) self.assertEqual(len(self._rrqlexpr_rset('add', 'concerne')), len(add_concerne_rqlexpr)) # * migrschema involve: - # * 7 rqlexprs deletion (2 in (Affaire read + Societe + travaille) + 1 - # in para attribute) + # * 7 erqlexprs deletions (2 in (Affaire + Societe + Note.para) + 1 Note.something + # * 2 rrqlexprs deletions (travaille) # * 1 update (Affaire update) # * 2 new (Note add, ecrit_par add) - # * 2 implicit new for attributes update_permission (Note.para, Personne.test) + # * 2 implicit new for attributes (Note.para, Person.test) # remaining orphan rql expr which should be deleted at commit (composite relation) - self.assertEqual(cursor.execute('Any COUNT(X) WHERE X is RQLExpression, ' - 'NOT ET1 read_permission X, NOT ET2 add_permission X, ' - 'NOT ET3 delete_permission X, NOT ET4 update_permission X')[0][0], - 7+1) + # unattached expressions -> pending deletion on commit + self.assertEqual(cursor.execute('Any COUNT(X) WHERE X is RQLExpression, X exprtype "ERQLExpression",' + 'NOT ET1 read_permission X, NOT ET2 add_permission X, ' + 'NOT ET3 delete_permission X, NOT ET4 update_permission X')[0][0], + 7) + self.assertEqual(cursor.execute('Any COUNT(X) WHERE X is RQLExpression, X exprtype "RRQLExpression",' + 'NOT ET1 read_permission X, NOT ET2 add_permission X, ' + 'NOT ET3 delete_permission X, NOT ET4 update_permission X')[0][0], + 2) # finally self.assertEqual(cursor.execute('Any COUNT(X) WHERE X is RQLExpression')[0][0], - nbrqlexpr_start + 1 + 2 + 2) + nbrqlexpr_start + 1 + 2 + 2 + 2) self.mh.commit() # unique_together test self.assertEqual(len(self.schema.eschema('Personne')._unique_together), 1) diff -r 2790fb8f7e03 -r e83cbc116352 server/test/unittest_querier.py --- a/server/test/unittest_querier.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/test/unittest_querier.py Fri Jan 10 17:12:20 2014 +0100 @@ -576,7 +576,7 @@ self.assertListEqual(rset.rows, [[u'description_format', 12], [u'description', 13], - [u'name', 16], + [u'name', 17], [u'created_by', 43], [u'creation_date', 43], [u'cw_source', 43], diff -r 2790fb8f7e03 -r e83cbc116352 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/test/unittest_repository.py Fri Jan 10 17:12:20 2014 +0100 @@ -278,7 +278,7 @@ 'creation_date', 'modification_date', 'cwuri', 'owned_by', 'created_by', 'cw_source', 'update_permission', 'read_permission', - 'in_basket')) + 'add_permission', 'in_basket')) self.assertListEqual(['relation_type', 'from_entity', 'to_entity', 'constrained_by', diff -r 2790fb8f7e03 -r e83cbc116352 server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/test/unittest_rql2sql.py Fri Jan 10 17:12:20 2014 +0100 @@ -1070,68 +1070,6 @@ FROM cw_Personne AS _P'''), ] -SYMMETRIC = [ - ('Any P WHERE X eid 0, X connait P', - '''SELECT DISTINCT _P.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _P -WHERE (rel_connait0.eid_from=0 AND rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_to=0 AND rel_connait0.eid_from=_P.cw_eid)''' - ), - - ('Any P WHERE X connait P', - '''SELECT DISTINCT _P.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _P -WHERE (rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_from=_P.cw_eid)''' - ), - - ('Any X WHERE X connait P', - '''SELECT DISTINCT _X.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _X -WHERE (rel_connait0.eid_from=_X.cw_eid OR rel_connait0.eid_to=_X.cw_eid)''' - ), - - ('Any P WHERE X eid 0, NOT X connait P', - '''SELECT _P.cw_eid -FROM cw_Personne AS _P -WHERE NOT (EXISTS(SELECT 1 FROM connait_relation AS rel_connait0 WHERE (rel_connait0.eid_from=0 AND rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_to=0 AND rel_connait0.eid_from=_P.cw_eid)))'''), - - ('Any P WHERE NOT X connait P', - '''SELECT _P.cw_eid -FROM cw_Personne AS _P -WHERE NOT (EXISTS(SELECT 1 FROM connait_relation AS rel_connait0 WHERE (rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_from=_P.cw_eid)))'''), - - ('Any X WHERE NOT X connait P', - '''SELECT _X.cw_eid -FROM cw_Personne AS _X -WHERE NOT (EXISTS(SELECT 1 FROM connait_relation AS rel_connait0 WHERE (rel_connait0.eid_from=_X.cw_eid OR rel_connait0.eid_to=_X.cw_eid)))'''), - - ('Any P WHERE X connait P, P nom "nom"', - '''SELECT DISTINCT _P.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _P -WHERE (rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_from=_P.cw_eid) AND _P.cw_nom=nom'''), - - ('Any X WHERE X connait P, P nom "nom"', - '''SELECT DISTINCT _X.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _P, cw_Personne AS _X -WHERE (rel_connait0.eid_from=_X.cw_eid AND rel_connait0.eid_to=_P.cw_eid OR rel_connait0.eid_to=_X.cw_eid AND rel_connait0.eid_from=_P.cw_eid) AND _P.cw_nom=nom''' - ), - - ('DISTINCT Any P WHERE P connait S OR S connait P, S nom "chouette"', - '''SELECT DISTINCT _P.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _P, cw_Personne AS _S -WHERE (rel_connait0.eid_from=_P.cw_eid AND rel_connait0.eid_to=_S.cw_eid OR rel_connait0.eid_to=_P.cw_eid AND rel_connait0.eid_from=_S.cw_eid) AND _S.cw_nom=chouette''' - ) - ] - -SYMMETRIC_WITH_LIMIT = [ - ('Any X ORDERBY X DESC LIMIT 9 WHERE E eid 0, E connait X', - '''SELECT DISTINCT _X.cw_eid -FROM connait_relation AS rel_connait0, cw_Personne AS _X -WHERE (rel_connait0.eid_from=0 AND rel_connait0.eid_to=_X.cw_eid OR rel_connait0.eid_to=0 AND rel_connait0.eid_from=_X.cw_eid) -ORDER BY 1 DESC -LIMIT 9''' - ), -] - INLINE = [ ('Any P WHERE N eid 1, N ecrit_par P, NOT P owned_by P2', @@ -1578,10 +1516,6 @@ rqlst = self._prepare(rql) self.assertRaises(BadRQLQuery, self.o.generate, rqlst) - def test_symmetric(self): - for t in self._parse(SYMMETRIC + SYMMETRIC_WITH_LIMIT): - yield t - def test_inline(self): for t in self._parse(INLINE): yield t @@ -1806,10 +1740,6 @@ '''SELECT DATEPART(WEEKDAY, _P.cw_creation_date) FROM cw_Personne AS _P''') - def test_symmetric(self): - for t in self._parse(SYMMETRIC): - yield t - def test_basic_parse(self): for t in self._parse(BASIC):# + BASIC_WITH_LIMIT): yield t diff -r 2790fb8f7e03 -r e83cbc116352 server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/test/unittest_schemaserial.py Fri Jan 10 17:12:20 2014 +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 @@ -132,7 +133,12 @@ 'description': u'groups allowed to add entities/relations of this type', 'composite': None, 'ordernum': 9999, 'cardinality': u'**'}), ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)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'rql expression allowing to add entities/relations of this type', 'composite': 'subject', 'ordernum': 9999, 'cardinality': u'*?'})], + 'description': u'rql expression allowing to add entities/relations of this type', 'composite': 'subject', 'ordernum': 9999, 'cardinality': u'*?'}), + ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)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', + {'cardinality': u'**', 'composite': None, 'description': u'groups allowed to add entities/relations of this type', + 'oe': None, 'ordernum': 9999, 'rt': None, 'se': None}), + ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)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', + {'cardinality': u'*?', 'composite': u'subject', 'description': u'rql expression allowing to add entities/relations of this type', 'oe': None, 'ordernum': 9999, 'rt': None, 'se': None})], list(rschema2rql(schema.rschema('add_permission'), cstrtypemap))) def test_rschema2rql3(self): @@ -188,7 +194,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 +202,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', @@ -265,6 +271,7 @@ self.assertListEqual([('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 1}), ('SET X read_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 2}), + ('SET X add_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0}), ('SET X update_permission Y WHERE Y eid %(g)s, X eid %(x)s', {'g': 0})], [(rql, kwargs) for rql, kwargs in erperms2rql(schema.rschema('name').rdef('CWEType', 'String'), diff -r 2790fb8f7e03 -r e83cbc116352 server/test/unittest_security.py --- a/server/test/unittest_security.py Fri Jun 21 14:22:39 2013 +0200 +++ b/server/test/unittest_security.py Fri Jan 10 17:12:20 2014 +0100 @@ -413,6 +413,16 @@ self.commit() cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}) self.commit() + cu.execute("INSERT Note X: X something 'A'") + self.assertRaises(Unauthorized, self.commit) + cu.execute("INSERT Note X: X para 'zogzog', X something 'A'") + self.commit() + note = cu.execute("INSERT Note X").get_entity(0,0) + self.commit() + note.cw_set(something=u'B') + self.commit() + note.cw_set(something=None, para=u'zogzog') + self.commit() def test_attribute_read_security(self): # anon not allowed to see users'login, but they can see users diff -r 2790fb8f7e03 -r e83cbc116352 skeleton/i18n/en.po --- a/skeleton/i18n/en.po Fri Jun 21 14:22:39 2013 +0200 +++ b/skeleton/i18n/en.po Fri Jan 10 17:12:20 2014 +0100 @@ -5,4 +5,5 @@ "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" diff -r 2790fb8f7e03 -r e83cbc116352 skeleton/i18n/es.po --- a/skeleton/i18n/es.po Fri Jun 21 14:22:39 2013 +0200 +++ b/skeleton/i18n/es.po Fri Jan 10 17:12:20 2014 +0100 @@ -5,4 +5,5 @@ "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" diff -r 2790fb8f7e03 -r e83cbc116352 skeleton/i18n/fr.po --- a/skeleton/i18n/fr.po Fri Jun 21 14:22:39 2013 +0200 +++ b/skeleton/i18n/fr.po Fri Jan 10 17:12:20 2014 +0100 @@ -5,4 +5,5 @@ "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" diff -r 2790fb8f7e03 -r e83cbc116352 sobjects/notification.py --- a/sobjects/notification.py Fri Jun 21 14:22:39 2013 +0200 +++ b/sobjects/notification.py Fri Jan 10 17:12:20 2014 +0100 @@ -206,7 +206,7 @@ kwargs.update({'user': self.user_data['login'], 'eid': entity.eid, 'etype': entity.dc_type(), - 'url': entity.absolute_url(), + 'url': entity.absolute_url(__secure__=True), 'title': entity.dc_long_title(),}) return kwargs diff -r 2790fb8f7e03 -r e83cbc116352 test/data/migration/0.1.0_web.py --- a/test/data/migration/0.1.0_web.py Fri Jun 21 14:22:39 2013 +0200 +++ /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 2790fb8f7e03 -r e83cbc116352 test/data/schema.py --- a/test/data/schema.py Fri Jun 21 14:22:39 2013 +0200 +++ b/test/data/schema.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 test/unittest_entity.py --- a/test/unittest_entity.py Fri Jun 21 14:22:39 2013 +0200 +++ b/test/unittest_entity.py Fri Jan 10 17:12:20 2014 +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') @@ -189,43 +207,43 @@ # testing basic fetch_attrs attribute self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB,AC ORDERBY AA ' - 'WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC') + 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X modification_date AC') # testing unknown attributes Personne.fetch_attrs = ('bloug', 'beep') - self.assertEqual(Personne.fetch_rql(user), 'Any X WHERE X is Personne') + self.assertEqual(Personne.fetch_rql(user), 'Any X WHERE X is_instance_of Personne') # testing one non final relation Personne.fetch_attrs = ('nom', 'prenom', 'travaille') self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB,AC,AD ORDERBY AA ' - 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD') + 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD') # testing two non final relations Personne.fetch_attrs = ('nom', 'prenom', 'travaille', 'evaluee') self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB,AC,AD,AE ORDERBY AA ' - 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' + 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' 'X evaluee AE?') # testing one non final relation with recursion Personne.fetch_attrs = ('nom', 'prenom', 'travaille') Societe.fetch_attrs = ('nom', 'evaluee') self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA,AF DESC ' - 'WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' + 'WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD, ' 'AC evaluee AE?, AE modification_date AF' ) # testing symmetric relation Personne.fetch_attrs = ('nom', 'connait') self.assertEqual(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA ' - 'WHERE X is Personne, X nom AA, X connait AB?') + 'WHERE X is_instance_of Personne, X nom AA, X connait AB?') # testing optional relation peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '?*' Personne.fetch_attrs = ('nom', 'prenom', 'travaille') Societe.fetch_attrs = ('nom',) self.assertEqual(Personne.fetch_rql(user), - 'Any X,AA,AB,AC,AD ORDERBY AA WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD') + 'Any X,AA,AB,AC,AD ORDERBY AA WHERE X is_instance_of Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD') # testing relation with cardinality > 1 peschema.subjrels['travaille'].rdef(peschema, seschema).cardinality = '**' self.assertEqual(Personne.fetch_rql(user), - 'Any X,AA,AB ORDERBY AA WHERE X is Personne, X nom AA, X prenom AB') + 'Any X,AA,AB ORDERBY AA WHERE X is_instance_of Personne, X nom AA, X prenom AB') # XXX test unauthorized attribute finally: # fetch_attrs restored by generic tearDown @@ -289,7 +307,7 @@ rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0] self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' 'WHERE NOT A use_email O, S eid %(x)s, ' - 'O is EmailAddress, O address AA, O alias AB, O modification_date AC') + 'O is_instance_of EmailAddress, O address AA, O alias AB, O modification_date AC') def test_unrelated_rql_security_1_user(self): req = self.request() @@ -299,7 +317,7 @@ rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0] self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' 'WHERE NOT A use_email O, S eid %(x)s, ' - 'O is EmailAddress, O address AA, O alias AB, O modification_date AC') + 'O is_instance_of EmailAddress, O address AA, O alias AB, O modification_date AC') user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0) rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0] self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC ' @@ -320,7 +338,7 @@ email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0) rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0] self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ' - 'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, ' + 'WHERE NOT S use_email O, O eid %(x)s, S is_instance_of CWUser, ' 'S login AA, S firstname AB, S surname AC, S modification_date AD') self.login('anon') rperms = self.schema['EmailAddress'].permissions['read'] @@ -354,7 +372,7 @@ rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0] self.assertEqual( rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE ' - 'O is Personne, O nom AA, O prenom AB, O modification_date AC') + 'O is_instance_of Personne, O nom AA, O prenom AB, O modification_date AC') def test_unrelated_rql_constraints_creation_object(self): person = self.vreg['etypes'].etype_class('Personne')(self.request()) @@ -374,7 +392,7 @@ person = self.vreg['etypes'].etype_class('Personne')(self.request()) rql = person.cw_unrelated_rql('connait', 'Personne', 'subject')[0] self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE ' - 'O is Personne, O nom AA, O prenom AB, ' + 'O is_instance_of Personne, O nom AA, O prenom AB, ' 'O modification_date AC') def test_unrelated_rql_constraints_edition_subject(self): @@ -417,7 +435,7 @@ rql, args = person.cw_unrelated_rql('actionnaire', 'Societe', 'subject', lt_infos=lt_infos) self.assertEqual(u'Any O ORDERBY O WHERE NOT A actionnaire O, ' - u'O is Societe, NOT EXISTS(O eid %(O)s), ' + u'O is_instance_of Societe, NOT EXISTS(O eid %(O)s), ' u'A is Personne', rql) self.assertEqual({'O': soc.eid}, args) @@ -430,7 +448,7 @@ rql, args = soc.cw_unrelated_rql('actionnaire', 'Personne', 'object', lt_infos=lt_infos) self.assertEqual(u'Any S ORDERBY S WHERE NOT S actionnaire A, ' - u'S is Personne, NOT EXISTS(S eid %(S)s), ' + u'S is_instance_of Personne, NOT EXISTS(S eid %(S)s), ' u'A is Societe', rql) self.assertEqual({'S': person.eid}, args) @@ -443,7 +461,7 @@ rql, args = soc.cw_unrelated_rql('dirige', 'Personne', 'object', lt_infos=lt_infos) self.assertEqual(u'Any S ORDERBY S WHERE NOT S dirige A, ' - u'S is Personne, EXISTS(S eid %(S)s), ' + u'S is_instance_of Personne, EXISTS(S eid %(S)s), ' u'A is Societe', rql) self.assertEqual({'S': person.eid}, args) @@ -453,7 +471,7 @@ self.vreg['etypes'].etype_class('Personne').fetch_attrs = () soc = req.create_entity('Societe', nom=u'logilab') rql, args = person.cw_unrelated_rql('associe', 'Personne', 'subject') - self.assertEqual(u'Any O ORDERBY O WHERE O is Personne', rql) + self.assertEqual(u'Any O ORDERBY O WHERE O is_instance_of Personne', rql) self.assertEqual({}, args) def test_unrelated_rql_s_linkto_s_unused_info(self): @@ -464,7 +482,7 @@ lt_infos = {('dirige', 'subject'): [other_p.eid]} rql, args = person.cw_unrelated_rql('associe', 'Personne', 'subject', lt_infos=lt_infos) - self.assertEqual(u'Any O ORDERBY O WHERE O is Personne', rql) + self.assertEqual(u'Any O ORDERBY O WHERE O is_instance_of Personne', rql) def test_unrelated_base(self): req = self.request() @@ -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 2790fb8f7e03 -r e83cbc116352 test/unittest_mail.py --- a/test/unittest_mail.py Fri Jun 21 14:22:39 2013 +0200 +++ b/test/unittest_mail.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 test/unittest_migration.py --- a/test/unittest_migration.py Fri Jun 21 14:22:39 2013 +0200 +++ b/test/unittest_migration.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 test/unittest_req.py --- a/test/unittest_req.py Fri Jun 21 14:22:39 2013 +0200 +++ b/test/unittest_req.py Fri Jan 10 17:12:20 2014 +0100 @@ -18,7 +18,7 @@ from logilab.common.testlib import TestCase, unittest_main from cubicweb import ObjectNotFound -from cubicweb.req import RequestSessionBase +from cubicweb.req import RequestSessionBase, FindEntityError from cubicweb.devtools.testlib import CubicWebTC from cubicweb import Unauthorized @@ -53,11 +53,97 @@ class RequestCWTC(CubicWebTC): + + def test_base_url(self): + base_url = self.config['base-url'] + self.assertEqual(self.session.base_url(), base_url) + assert 'https-url' not in self.config + self.assertEqual(self.session.base_url(secure=True), base_url) + secure_base_url = base_url.replace('http', 'https') + self.config.global_set_option('https-url', secure_base_url) + self.assertEqual(self.session.base_url(secure=True), secure_base_url) + def test_view_catch_ex(self): req = self.request() rset = self.execute('CWUser X WHERE X login "hop"') self.assertEqual(req.view('oneline', rset, 'null'), '') self.assertRaises(ObjectNotFound, req.view, 'onelinee', rset, 'null') + def test_find_one_entity(self): + self.request().create_entity( + 'CWUser', login=u'cdevienne', upassword=u'cdevienne', + surname=u'de Vienne', firstname=u'Christophe', + in_group=self.request().find('CWGroup', name=u'users').one()) + + self.request().create_entity( + 'CWUser', login=u'adim', upassword='adim', surname=u'di mascio', + firstname=u'adrien', + in_group=self.request().find('CWGroup', name=u'users').one()) + + u = self.request().find_one_entity('CWUser', login=u'cdevienne') + self.assertEqual(u.firstname, u"Christophe") + + with self.assertRaises(FindEntityError): + self.request().find_one_entity('CWUser', login=u'patanok') + + with self.assertRaises(FindEntityError): + self.request().find_one_entity('CWUser') + + def test_find_entities(self): + self.request().create_entity( + 'CWUser', login=u'cdevienne', upassword=u'cdevienne', + surname=u'de Vienne', firstname=u'Christophe', + in_group=self.request().find('CWGroup', name=u'users').one()) + + self.request().create_entity( + 'CWUser', login=u'adim', upassword='adim', surname=u'di mascio', + firstname=u'adrien', + in_group=self.request().find('CWGroup', name=u'users').one()) + + l = list(self.request().find_entities('CWUser', login=u'cdevienne')) + self.assertEqual(1, len(l)) + self.assertEqual(l[0].firstname, u"Christophe") + + l = list(self.request().find_entities('CWUser', login=u'patanok')) + self.assertEqual(0, len(l)) + + l = list(self.request().find_entities('CWUser')) + self.assertEqual(4, len(l)) + + def test_find(self): + self.request().create_entity( + 'CWUser', login=u'cdevienne', upassword=u'cdevienne', + surname=u'de Vienne', firstname=u'Christophe', + in_group=self.request().find('CWGroup', name=u'users').one()) + + self.request().create_entity( + 'CWUser', login=u'adim', upassword='adim', surname=u'di mascio', + firstname=u'adrien', + in_group=self.request().find('CWGroup', name=u'users').one()) + + u = self.request().find('CWUser', login=u'cdevienne').one() + self.assertEqual(u.firstname, u"Christophe") + + users = list(self.request().find('CWUser').entities()) + self.assertEqual(len(users), 4) + + groups = list( + self.request().find('CWGroup', reverse_in_group=u).entities()) + self.assertEqual(len(groups), 1) + self.assertEqual(groups[0].name, u'users') + + users = self.request().find('CWUser', in_group=groups[0]).entities() + users = list(users) + self.assertEqual(len(users), 2) + + with self.assertRaises(AssertionError): + self.request().find('CWUser', chapeau=u"melon") + + with self.assertRaises(AssertionError): + self.request().find('CWUser', reverse_buddy=users[0]) + + with self.assertRaises(NotImplementedError): + self.request().find('CWUser', in_group=[1, 2]) + if __name__ == '__main__': unittest_main() diff -r 2790fb8f7e03 -r e83cbc116352 test/unittest_rqlrewrite.py --- a/test/unittest_rqlrewrite.py Fri Jun 21 14:22:39 2013 +0200 +++ b/test/unittest_rqlrewrite.py Fri Jan 10 17:12:20 2014 +0100 @@ -486,6 +486,13 @@ rqlst = parse('Any A, R WHERE A ref R, S is Affaire') rewrite(rqlst, {('A', 'X'): (c_ok, c_bad)}, {}) + def test_nonregr_is_instance_of(self): + user_expr = ERQLExpression('NOT X in_group AF, AF name "guests"') + rqlst = parse('Any O WHERE S use_email O, S is CWUser, O is_instance_of EmailAddress') + rewrite(rqlst, {('S', 'X'): (user_expr,)}, {}) + self.assertEqual(rqlst.as_string(), + 'Any O WHERE S use_email O, S is CWUser, O is EmailAddress, ' + 'EXISTS(NOT S in_group A, A name "guests", A is CWGroup)') from cubicweb.devtools.testlib import CubicWebTC from logilab.common.decorators import classproperty diff -r 2790fb8f7e03 -r e83cbc116352 test/unittest_rset.py --- a/test/unittest_rset.py Fri Jun 21 14:22:39 2013 +0200 +++ b/test/unittest_rset.py Fri Jan 10 17:12:20 2014 +0100 @@ -28,6 +28,8 @@ from cubicweb.devtools.testlib import CubicWebTC from cubicweb.rset import NotAnEntity, ResultSet, attr_desc_iterator +from cubicweb import NoResultError, MultipleResultsError + def pprelcachedict(d): res = {} @@ -368,6 +370,39 @@ attr = etype == 'Bookmark' and 'title' or 'name' self.assertEqual(entity.cw_attr_cache[attr], n) + def test_one(self): + self.request().create_entity('CWUser', login=u'cdevienne', + upassword=u'cdevienne', + surname=u'de Vienne', + firstname=u'Christophe') + e = self.execute('Any X WHERE X login "cdevienne"').one() + + self.assertEqual(e.surname, u'de Vienne') + + e = self.execute( + 'Any X, N WHERE X login "cdevienne", X surname N').one() + self.assertEqual(e.surname, u'de Vienne') + + e = self.execute( + 'Any N, X WHERE X login "cdevienne", X surname N').one(col=1) + self.assertEqual(e.surname, u'de Vienne') + + def test_one_no_rows(self): + with self.assertRaises(NoResultError): + self.execute('Any X WHERE X login "patanok"').one() + + def test_one_multiple_rows(self): + self.request().create_entity( + 'CWUser', login=u'cdevienne', upassword=u'cdevienne', + surname=u'de Vienne', firstname=u'Christophe') + + self.request().create_entity( + 'CWUser', login=u'adim', upassword='adim', surname=u'di mascio', + firstname=u'adrien') + + with self.assertRaises(MultipleResultsError): + self.execute('Any X WHERE X is CWUser').one() + def test_related_entity_optional(self): e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path') rset = self.execute('Any B,U,L WHERE B bookmarked_by U?, U login L') diff -r 2790fb8f7e03 -r e83cbc116352 view.py --- a/view.py Fri Jun 21 14:22:39 2013 +0200 +++ b/view.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 web/__init__.py --- a/web/__init__.py Fri Jun 21 14:22:39 2013 +0200 +++ b/web/__init__.py Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.ajax.box.js --- a/web/data/cubicweb.ajax.box.js Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.ajax.box.js Fri Jan 10 17:12:20 2014 +0100 @@ -81,12 +81,12 @@ }); $input.cwautocomplete(unrelated, {multiple: Boolean(separator)}); var buttons = DIV({'class' : "sgformbuttons"}, - A({href : "javascript: noop();", + A({href : "javascript: $.noop();", onclick : cw.utils.strFuncCall('ajaxBoxValidateSelectorInput', boxid, eid, separator, addfname, msg)}, oklabel), ' / ', - A({'href' : "javascript: noop();", + A({'href' : "javascript: $.noop();", 'onclick' : '$("#' + holderid + '").empty()'}, cancellabel)); holder.append(buttons); diff -r 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.ajax.js --- a/web/data/cubicweb.ajax.js Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.ajax.js Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.calendar.css --- a/web/data/cubicweb.calendar.css Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.calendar.css Fri Jan 10 17:12:20 2014 +0100 @@ -231,6 +231,8 @@ font-weight:bold; padding-bottom:0.2em; background: %(incontextBoxBodyBgColor)s; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } .calendar th.month a{ diff -r 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.compat.js --- a/web/data/cubicweb.compat.js Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.compat.js Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.css --- a/web/data/cubicweb.css Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.css Fri Jan 10 17:12:20 2014 +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; @@ -358,17 +354,20 @@ div.shadow{ height: 14px; - background: url("shadow.gif") no-repeat top right; } div.sideBoxTitle { background: %(incontextBoxBodyBg)s; display: block; font-weight: bold; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } div.sideBox { margin-bottom: 1em; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } ul.sideBox, @@ -403,6 +402,8 @@ div.boxTitle { overflow: hidden; font-weight: bold; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } div.boxTitle span { @@ -467,7 +468,10 @@ #navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{ height: 14px; - background: url("shadow.gif") no-repeat top right; +} + +.boxBody, .boxTitle, #pageContent, #appMsg { + box-shadow: 1px 1px 3px Gray; } /* boxes lists and menus */ diff -r 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.facets.css --- a/web/data/cubicweb.facets.css Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.facets.css Fri Jan 10 17:12:20 2014 +0100 @@ -8,6 +8,8 @@ background: #fff; padding: %(facet_Padding)s; margin-bottom: %(facet_MarginBottom)s; + border-top-left-radius: 5px; + border-bottom-right-radius: 7px; } .facetGroup { diff -r 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.facets.js --- a/web/data/cubicweb.facets.js Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.facets.js Fri Jan 10 17:12:20 2014 +0100 @@ -169,7 +169,6 @@ if ($('#'+divid).length) { var $loadingDiv = $(DIV({id:'facetLoading'}, facetLoadingMsg)); - $loadingDiv.corner(); $($('#'+divid).get(0).parentNode).append($loadingDiv); } form.find('div.facet').each(function() { @@ -327,7 +326,6 @@ if ($('div.facetBody').length) { var $loadingDiv = $(DIV({id:'facetLoading'}, facetLoadingMsg)); - $loadingDiv.corner(); $('body').append($loadingDiv); } }); diff -r 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.htmlhelpers.js --- a/web/data/cubicweb.htmlhelpers.js Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.htmlhelpers.js Fri Jan 10 17:12:20 2014 +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('&'); @@ -195,19 +195,4 @@ } } } -//============= page loading events ==========================================// -cw.rounded = [['div.sideBoxBody', 'bottom 6px'], - ['div.boxTitle, div.sideBoxTitle, th.month', 'top 6px']]; -function roundedCorners(node) { - if (jQuery.fn.corner !== undefined) { - node = jQuery(node); - for (var r = 0; r < cw.rounded.length; r++) { - node.find(cw.rounded[r][0]).corner(cw.rounded[r][1]); - } - } -} - -jQuery(document).ready(function() { - roundedCorners(this.body); -}); diff -r 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.js --- a/web/data/cubicweb.js Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.js Fri Jan 10 17:12:20 2014 +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 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.old.css --- a/web/data/cubicweb.old.css Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.old.css Fri Jan 10 17:12:20 2014 +0100 @@ -411,13 +411,14 @@ div.shadow{ height: 14px; - background: url("shadow.gif") no-repeat top right; } div.sideBoxTitle { background: #cfceb7; display: block; font: bold 100% Georgia; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } div.sideBox { @@ -434,6 +435,8 @@ div.sideBoxBody { padding: 0.2em 5px; background: #eeedd9; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; } div.sideBoxBody a { @@ -457,6 +460,8 @@ div.boxTitle { overflow: hidden; font-weight: bold; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } div.boxTitle span { @@ -521,7 +526,14 @@ #navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{ height: 14px; - background: url("shadow.gif") no-repeat top right; +} + +.navboxes { + padding: 2px; +} + +.boxBody, .boxTitle, #pageContent, #appMsg { + box-shadow: 1px 1px 3px Gray; } /* boxes lists and menus */ diff -r 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.pictograms.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/cubicweb.pictograms.css Fri Jan 10 17:12:20 2014 +0100 @@ -0,0 +1,343 @@ +/*The included Entypo font have been created by Daniel Bruce (www.entypo.com) as +proposed by fontello (https://github.com/fontello/entypo). +The Entypo pictograms are licensed under CC BY 3.0 and the font under +SIL Open Font License.*/ +@font-face { + font-family: 'entypo'; + src: url('entypo.eot?8644018'); + src: url('entypo.eot?8644018#iefix') format('embedded-opentype'), + url('entypo.woff?8644018') format('woff'), + url('entypo.ttf?8644018') format('truetype'), + url('entypo.svg?8644018#entypo') format('svg'); + font-weight: normal; + font-style: normal; +} +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'entypo'; + src: url('../font/entypo.svg?8644018#entypo') format('svg'); + } +} +*/ + +[class^="icon-"]:before, +[class*=" icon-"]:before { + font-family: "entypo"; + font-style: normal; + font-weight: normal; + speak: none; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .1em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - magrins should be symmetric */ + /* remove if not needed */ + margin-left: .1em; + + /* you can be more comfortable with increased icons size */ + font-size: 160%; + vertical-align: middle; + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} + +.icon-note:before { content: '\266a'; } /* '♪' */ +.icon-note-beamed:before { content: '\266b'; } /* '♫' */ +.icon-music:before { content: '\1f3b5'; } /* '\1f3b5' */ +.icon-search:before { content: '\1f50d'; } /* '\1f50d' */ +.icon-flashlight:before { content: '\1f526'; } /* '\1f526' */ +.icon-mail:before { content: '\2709'; } /* '✉' */ +.icon-heart:before { content: '\2665'; } /* '♥' */ +.icon-heart-empty:before { content: '\2661'; } /* '♡' */ +.icon-star:before { content: '\2605'; } /* '★' */ +.icon-star-empty:before { content: '\2606'; } /* '☆' */ +.icon-user:before { content: '\1f464'; } +.icon-users:before { content: '\1f465'; } +.icon-user-add:before { content: '\e700'; } /* '' */ +.icon-video:before { content: '\1f3ac'; } +.icon-picture:before { content: '\1f304'; } +.icon-camera:before { content: '\1f4f7'; } +.icon-layout:before { content: '\268f'; } /* '⚏' */ +.icon-menu:before { content: '\2630'; } /* '☰' */ +.icon-check:before { content: '\2713'; } /* '✓' */ +.icon-cancel:before { content: '\2715'; } /* '✕' */ +.icon-cancel-circled:before { content: '\2716'; } /* '✖' */ +.icon-cancel-squared:before { content: '\274e'; } /* '❎' */ +.icon-plus:before { content: '\2b'; } /* '+' */ +.icon-plus-circled:before { content: '\2795'; } /* '➕' */ +.icon-plus-squared:before { content: '\229e'; } /* '⊞' */ +.icon-minus:before { content: '\2d'; } /* '-' */ +.icon-minus-circled:before { content: '\2796'; } /* '➖' */ +.icon-minus-squared:before { content: '\229f'; } /* '⊟' */ +.icon-help:before { content: '\2753'; } /* '❓' */ +.icon-help-circled:before { content: '\e704'; } /* '' */ +.icon-info:before { content: '\2139'; } /* 'ℹ' */ +.icon-info-circled:before { content: '\e705'; } /* '' */ +.icon-back:before { content: '\1f519'; } +.icon-home:before { content: '\2302'; } /* '⌂' */ +.icon-link:before { content: '\1f517'; } +.icon-attach:before { content: '\1f4ce'; } +.icon-lock:before { content: '\1f512'; } +.icon-lock-open:before { content: '\1f513'; } +.icon-eye:before { content: '\e70a'; } /* '' */ +.icon-tag:before { content: '\e70c'; } /* '' */ +.icon-bookmark:before { content: '\1f516'; } +.icon-bookmarks:before { content: '\1f4d1'; } +.icon-flag:before { content: '\2691'; } /* '⚑' */ +.icon-thumbs-up:before { content: '\1f44d'; } +.icon-thumbs-down:before { content: '\1f44e'; } +.icon-download:before { content: '\1f4e5'; } +.icon-upload:before { content: '\1f4e4'; } +.icon-upload-cloud:before { content: '\e711'; } /* '' */ +.icon-reply:before { content: '\e712'; } /* '' */ +.icon-reply-all:before { content: '\e713'; } /* '' */ +.icon-forward:before { content: '\27a6'; } /* '➦' */ +.icon-quote:before { content: '\275e'; } /* '❞' */ +.icon-code:before { content: '\e714'; } /* '' */ +.icon-export:before { content: '\e715'; } /* '' */ +.icon-pencil:before { content: '\270e'; } /* '✎' */ +.icon-feather:before { content: '\2712'; } /* '✒' */ +.icon-print:before { content: '\e716'; } /* '' */ +.icon-retweet:before { content: '\e717'; } /* '' */ +.icon-keyboard:before { content: '\2328'; } /* '⌨' */ +.icon-comment:before { content: '\e718'; } /* '' */ +.icon-chat:before { content: '\e720'; } /* '' */ +.icon-bell:before { content: '\1f514'; } +.icon-attention:before { content: '\26a0'; } /* '⚠' */ +.icon-alert:before { content: '\1f4a5'; } +.icon-vcard:before { content: '\e722'; } /* '' */ +.icon-address:before { content: '\e723'; } /* '' */ +.icon-location:before { content: '\e724'; } /* '' */ +.icon-map:before { content: '\e727'; } /* '' */ +.icon-direction:before { content: '\27a2'; } /* '➢' */ +.icon-compass:before { content: '\e728'; } /* '' */ +.icon-cup:before { content: '\2615'; } /* '☕' */ +.icon-trash:before { content: '\e729'; } /* '' */ +.icon-doc:before { content: '\e730'; } /* '' */ +.icon-docs:before { content: '\e736'; } /* '' */ +.icon-doc-landscape:before { content: '\e737'; } /* '' */ +.icon-doc-text:before { content: '\1f4c4'; } +.icon-doc-text-inv:before { content: '\e731'; } /* '' */ +.icon-newspaper:before { content: '\1f4f0'; } +.icon-book-open:before { content: '\1f4d6'; } +.icon-book:before { content: '\1f4d5'; } +.icon-folder:before { content: '\1f4c1'; } +.icon-archive:before { content: '\e738'; } /* '' */ +.icon-box:before { content: '\1f4e6'; } +.icon-rss:before { content: '\e73a'; } /* '' */ +.icon-phone:before { content: '\1f4de'; } +.icon-cog:before { content: '\2699'; } /* '⚙' */ +.icon-tools:before { content: '\2692'; } /* '⚒' */ +.icon-share:before { content: '\e73c'; } /* '' */ +.icon-shareable:before { content: '\e73e'; } /* '' */ +.icon-basket:before { content: '\e73d'; } /* '' */ +.icon-bag:before { content: '\1f45c'; } +.icon-calendar:before { content: '\1f4c5'; } +.icon-login:before { content: '\e740'; } /* '' */ +.icon-logout:before { content: '\e741'; } /* '' */ +.icon-mic:before { content: '\1f3a4'; } +.icon-mute:before { content: '\1f507'; } +.icon-sound:before { content: '\1f50a'; } +.icon-volume:before { content: '\e742'; } /* '' */ +.icon-clock:before { content: '\1f554'; } +.icon-hourglass:before { content: '\23f3'; } /* '⏳' */ +.icon-lamp:before { content: '\1f4a1'; } +.icon-light-down:before { content: '\1f505'; } +.icon-light-up:before { content: '\1f506'; } +.icon-adjust:before { content: '\25d1'; } /* '◑' */ +.icon-block:before { content: '\1f6ab'; } +.icon-resize-full:before { content: '\e744'; } /* '' */ +.icon-resize-small:before { content: '\e746'; } /* '' */ +.icon-popup:before { content: '\e74c'; } /* '' */ +.icon-publish:before { content: '\e74d'; } /* '' */ +.icon-window:before { content: '\e74e'; } /* '' */ +.icon-arrow-combo:before { content: '\e74f'; } /* '' */ +.icon-down-circled:before { content: '\e758'; } /* '' */ +.icon-left-circled:before { content: '\e759'; } /* '' */ +.icon-right-circled:before { content: '\e75a'; } /* '' */ +.icon-up-circled:before { content: '\e75b'; } /* '' */ +.icon-down-open:before { content: '\e75c'; } /* '' */ +.icon-left-open:before { content: '\e75d'; } /* '' */ +.icon-right-open:before { content: '\e75e'; } /* '' */ +.icon-up-open:before { content: '\e75f'; } /* '' */ +.icon-down-open-mini:before { content: '\e760'; } /* '' */ +.icon-left-open-mini:before { content: '\e761'; } /* '' */ +.icon-right-open-mini:before { content: '\e762'; } /* '' */ +.icon-up-open-mini:before { content: '\e763'; } /* '' */ +.icon-down-open-big:before { content: '\e764'; } /* '' */ +.icon-left-open-big:before { content: '\e765'; } /* '' */ +.icon-right-open-big:before { content: '\e766'; } /* '' */ +.icon-up-open-big:before { content: '\e767'; } /* '' */ +.icon-down:before { content: '\2b07'; } /* '⬇' */ +.icon-left:before { content: '\2b05'; } /* '⬅' */ +.icon-right:before { content: '\27a1'; } /* '➡' */ +.icon-up:before { content: '\2b06'; } /* '⬆' */ +.icon-down-dir:before { content: '\25be'; } /* '▾' */ +.icon-left-dir:before { content: '\25c2'; } /* '◂' */ +.icon-right-dir:before { content: '\25b8'; } /* '▸' */ +.icon-up-dir:before { content: '\25b4'; } /* '▴' */ +.icon-down-bold:before { content: '\e4b0'; } /* '' */ +.icon-left-bold:before { content: '\e4ad'; } /* '' */ +.icon-right-bold:before { content: '\e4ae'; } /* '' */ +.icon-up-bold:before { content: '\e4af'; } /* '' */ +.icon-down-thin:before { content: '\2193'; } /* '↓' */ +.icon-left-thin:before { content: '\2190'; } /* '←' */ +.icon-right-thin:before { content: '\2192'; } /* '→' */ +.icon-up-thin:before { content: '\2191'; } /* '↑' */ +.icon-ccw:before { content: '\27f2'; } /* '⟲' */ +.icon-cw:before { content: '\27f3'; } /* '⟳' */ +.icon-arrows-ccw:before { content: '\1f504'; } +.icon-level-down:before { content: '\21b3'; } /* '↳' */ +.icon-level-up:before { content: '\21b0'; } /* '↰' */ +.icon-shuffle:before { content: '\1f500'; } +.icon-loop:before { content: '\1f501'; } +.icon-switch:before { content: '\21c6'; } /* '⇆' */ +.icon-play:before { content: '\25b6'; } /* '▶' */ +.icon-stop:before { content: '\25a0'; } /* '■' */ +.icon-pause:before { content: '\2389'; } /* '⎉' */ +.icon-record:before { content: '\26ab'; } /* '⚫' */ +.icon-to-end:before { content: '\23ed'; } /* '⏭' */ +.icon-to-start:before { content: '\23ee'; } /* '⏮' */ +.icon-fast-forward:before { content: '\23e9'; } /* '⏩' */ +.icon-fast-backward:before { content: '\23ea'; } /* '⏪' */ +.icon-progress-0:before { content: '\e768'; } /* '' */ +.icon-progress-1:before { content: '\e769'; } /* '' */ +.icon-progress-2:before { content: '\e76a'; } /* '' */ +.icon-progress-3:before { content: '\e76b'; } /* '' */ +.icon-target:before { content: '\1f3af'; } +.icon-palette:before { content: '\1f3a8'; } +.icon-list:before { content: '\e005'; } /* '' */ +.icon-list-add:before { content: '\e003'; } /* '' */ +.icon-signal:before { content: '\1f4f6'; } +.icon-trophy:before { content: '\1f3c6'; } +.icon-battery:before { content: '\1f50b'; } +.icon-back-in-time:before { content: '\e771'; } /* '' */ +.icon-monitor:before { content: '\1f4bb'; } +.icon-mobile:before { content: '\1f4f1'; } +.icon-network:before { content: '\e776'; } /* '' */ +.icon-cd:before { content: '\1f4bf'; } +.icon-inbox:before { content: '\e777'; } /* '' */ +.icon-install:before { content: '\e778'; } /* '' */ +.icon-globe:before { content: '\1f30e'; } +.icon-cloud:before { content: '\2601'; } /* '☁' */ +.icon-cloud-thunder:before { content: '\26c8'; } /* '⛈' */ +.icon-flash:before { content: '\26a1'; } /* '⚡' */ +.icon-moon:before { content: '\263d'; } /* '☽' */ +.icon-flight:before { content: '\2708'; } /* '✈' */ +.icon-paper-plane:before { content: '\e79b'; } /* '' */ +.icon-leaf:before { content: '\1f342'; } +.icon-lifebuoy:before { content: '\e788'; } /* '' */ +.icon-mouse:before { content: '\e789'; } /* '' */ +.icon-briefcase:before { content: '\1f4bc'; } +.icon-suitcase:before { content: '\e78e'; } /* '' */ +.icon-dot:before { content: '\e78b'; } /* '' */ +.icon-dot-2:before { content: '\e78c'; } /* '' */ +.icon-dot-3:before { content: '\e78d'; } /* '' */ +.icon-brush:before { content: '\e79a'; } /* '' */ +.icon-magnet:before { content: '\e7a1'; } /* '' */ +.icon-infinity:before { content: '\221e'; } /* '∞' */ +.icon-erase:before { content: '\232b'; } /* '⌫' */ +.icon-chart-pie:before { content: '\e751'; } /* '' */ +.icon-chart-line:before { content: '\1f4c8'; } +.icon-chart-bar:before { content: '\1f4ca'; } +.icon-chart-area:before { content: '\1f53e'; } +.icon-tape:before { content: '\2707'; } /* '✇' */ +.icon-graduation-cap:before { content: '\1f393'; } +.icon-language:before { content: '\e752'; } /* '' */ +.icon-ticket:before { content: '\1f3ab'; } +.icon-water:before { content: '\1f4a6'; } +.icon-droplet:before { content: '\1f4a7'; } +.icon-air:before { content: '\e753'; } /* '' */ +.icon-credit-card:before { content: '\1f4b3'; } +.icon-floppy:before { content: '\1f4be'; } +.icon-clipboard:before { content: '\1f4cb'; } +.icon-megaphone:before { content: '\1f4e3'; } +.icon-database:before { content: '\e754'; } /* '' */ +.icon-drive:before { content: '\e755'; } /* '' */ +.icon-bucket:before { content: '\e756'; } /* '' */ +.icon-thermometer:before { content: '\e757'; } /* '' */ +.icon-key:before { content: '\1f511'; } +.icon-flow-cascade:before { content: '\e790'; } /* '' */ +.icon-flow-branch:before { content: '\e791'; } /* '' */ +.icon-flow-tree:before { content: '\e792'; } /* '' */ +.icon-flow-line:before { content: '\e793'; } /* '' */ +.icon-flow-parallel:before { content: '\e794'; } /* '' */ +.icon-rocket:before { content: '\1f680'; } +.icon-gauge:before { content: '\e7a2'; } /* '' */ +.icon-traffic-cone:before { content: '\e7a3'; } /* '' */ +.icon-cc:before { content: '\e7a5'; } /* '' */ +.icon-cc-by:before { content: '\e7a6'; } /* '' */ +.icon-cc-nc:before { content: '\e7a7'; } /* '' */ +.icon-cc-nc-eu:before { content: '\e7a8'; } /* '' */ +.icon-cc-nc-jp:before { content: '\e7a9'; } /* '' */ +.icon-cc-sa:before { content: '\e7aa'; } /* '' */ +.icon-cc-nd:before { content: '\e7ab'; } /* '' */ +.icon-cc-pd:before { content: '\e7ac'; } /* '' */ +.icon-cc-zero:before { content: '\e7ad'; } /* '' */ +.icon-cc-share:before { content: '\e7ae'; } /* '' */ +.icon-cc-remix:before { content: '\e7af'; } /* '' */ +.icon-github:before { content: '\f300'; } /* '' */ +.icon-github-circled:before { content: '\f301'; } /* '' */ +.icon-flickr:before { content: '\f303'; } /* '' */ +.icon-flickr-circled:before { content: '\f304'; } /* '' */ +.icon-vimeo:before { content: '\f306'; } /* '' */ +.icon-vimeo-circled:before { content: '\f307'; } /* '' */ +.icon-twitter:before { content: '\f309'; } /* '' */ +.icon-twitter-circled:before { content: '\f30a'; } /* '' */ +.icon-facebook:before { content: '\f30c'; } /* '' */ +.icon-facebook-circled:before { content: '\f30d'; } /* '' */ +.icon-facebook-squared:before { content: '\f30e'; } /* '' */ +.icon-gplus:before { content: '\f30f'; } /* '' */ +.icon-gplus-circled:before { content: '\f310'; } /* '' */ +.icon-pinterest:before { content: '\f312'; } /* '' */ +.icon-pinterest-circled:before { content: '\f313'; } /* '' */ +.icon-tumblr:before { content: '\f315'; } /* '' */ +.icon-tumblr-circled:before { content: '\f316'; } /* '' */ +.icon-linkedin:before { content: '\f318'; } /* '' */ +.icon-linkedin-circled:before { content: '\f319'; } /* '' */ +.icon-dribbble:before { content: '\f31b'; } /* '' */ +.icon-dribbble-circled:before { content: '\f31c'; } /* '' */ +.icon-stumbleupon:before { content: '\f31e'; } /* '' */ +.icon-stumbleupon-circled:before { content: '\f31f'; } /* '' */ +.icon-lastfm:before { content: '\f321'; } /* '' */ +.icon-lastfm-circled:before { content: '\f322'; } /* '' */ +.icon-rdio:before { content: '\f324'; } /* '' */ +.icon-rdio-circled:before { content: '\f325'; } /* '' */ +.icon-spotify:before { content: '\f327'; } /* '' */ +.icon-spotify-circled:before { content: '\f328'; } /* '' */ +.icon-qq:before { content: '\f32a'; } /* '' */ +.icon-instagram:before { content: '\f32d'; } /* '' */ +.icon-dropbox:before { content: '\f330'; } /* '' */ +.icon-evernote:before { content: '\f333'; } /* '' */ +.icon-flattr:before { content: '\f336'; } /* '' */ +.icon-skype:before { content: '\f339'; } /* '' */ +.icon-skype-circled:before { content: '\f33a'; } /* '' */ +.icon-renren:before { content: '\f33c'; } /* '' */ +.icon-sina-weibo:before { content: '\f33f'; } /* '' */ +.icon-paypal:before { content: '\f342'; } /* '' */ +.icon-picasa:before { content: '\f345'; } /* '' */ +.icon-soundcloud:before { content: '\f348'; } /* '' */ +.icon-mixi:before { content: '\f34b'; } /* '' */ +.icon-behance:before { content: '\f34e'; } /* '' */ +.icon-google-circles:before { content: '\f351'; } /* '' */ +.icon-vkontakte:before { content: '\f354'; } /* '' */ +.icon-smashing:before { content: '\f357'; } /* '' */ +.icon-sweden:before { content: '\f601'; } /* '' */ +.icon-db-shape:before { content: '\f600'; } /* '' */ +.icon-logo-db:before { content: '\f603'; } /* '' */ + diff -r 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.rhythm.js --- a/web/data/cubicweb.rhythm.js Fri Jun 21 14:22:39 2013 +0200 +++ /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 2790fb8f7e03 -r e83cbc116352 web/data/cubicweb.widgets.js --- a/web/data/cubicweb.widgets.js Fri Jun 21 14:22:39 2013 +0200 +++ b/web/data/cubicweb.widgets.js Fri Jan 10 17:12:20 2014 +0100 @@ -92,7 +92,8 @@ if (($(instanceData.userInput).attr('cubicweb:initialvalue') !== undefined) && !instanceData.hiddenInput){ hiHandlers.initializeHiddenInput(instanceData); } - $.ui.autocomplete.prototype._search = methods.search; + $.ui.autocomplete.prototype._value = methods._value; + $.data(this, 'settings', settings); if (settings.multiple) { $.ui.autocomplete.filter = methods.multiple.makeFilter(this); $(this).bind({ @@ -125,6 +126,20 @@ }); }, + _value: function() { + /* We extend the widget with the ability to lookup and + handle several terms at once ('multiple' option). E.g.: + toto, titi, tu.... The autocompletion must be + performed only on the last of such a list of terms. + */ + var settings = $(this.element).data('settings'); + var value = this.valueMethod.apply( this.element, arguments ); + if (settings.multiple & arguments.length === 0) { + return extractLast(value); + } + return value + }, + multiple: { focus: function() { // prevent value inserted on focus @@ -140,7 +155,7 @@ return false; }, keydown: function(evt) { - if ($(this).data('autocomplete').menu.active && evt.keyCode == $.ui.keyCode.TAB) { + if (evt.keyCode == $.ui.keyCode.TAB) { evt.preventDefault(); } }, @@ -161,13 +176,7 @@ methods.resetValues(instanceData); } }, - search: function(value) { - this.element.addClass("ui-autocomplete-loading"); - if (this.options.multiple) { - value = extractLast(value); - } - this.source({term: value}, this.response); - }, + ensureExactMatch: function(evt) { var instanceData = $(this).data('cwautocomplete'); if (evt.keyCode == $.ui.keyCode.ENTER || evt.keyCode == $.ui.keyCode.TAB) { @@ -179,6 +188,7 @@ } } }, + resetValues: function(instanceData){ $(instanceData.userInput).val(''); $(instanceData.hiddenInput).val(''); @@ -544,105 +554,77 @@ // IE things can not handle hide/show options on select, this cloned list solition (should propably have 2 widgets) (function ($) { - var defaultSettings = { - bindDblClick: true - }; + var methods = { - __init__: function(fromSelect, toSelect, options) { - var settings = $.extend({}, defaultSettings, options); - var bindDblClick = settings['bindDblClick']; - var $fromNode = $(cw.jqNode(fromSelect)); - var clonedSelect = $fromNode.clone(); - var $toNode = $(cw.jqNode(toSelect)); - var $addButton = $(this.find('.cwinoutadd')[0]); - var $removeButton = $(this.find('.cwinoutremove')[0]); - // bind buttons - var name = this.attr('id'); - var instanceData = {'fromNode':fromSelect, - 'toNode':toSelect, - 'cloned':clonedSelect, - 'bindDblClick':bindDblClick, - 'name': name}; - $addButton.bind('click', {'instanceData':instanceData}, methods.inOutWidgetAddValues); - $removeButton.bind('click', {'instanceData':instanceData}, methods.inOutWidgetRemoveValues); - if(bindDblClick){ - $toNode.bind('dblclick', {'instanceData': instanceData}, methods.inOutWidgetRemoveValues); - } - methods.inOutWidgetRemplaceSelect($fromNode, $toNode, clonedSelect, bindDblClick, name); - }, + __init__: function(fromSelect, toSelect) { + // closed over state + var state = {'$fromNode' : $(cw.escape('#' + fromSelect)), + '$toNode' : $(cw.escape('#' + toSelect)), + 'name' : this.attr('id')}; - inOutWidgetRemplaceSelect: function($fromNode, $toNode, clonedSelect, bindDblClick, name){ - var $newSelect = clonedSelect.clone(); - $toNode.find('option').each(function() { - $newSelect.find('$(this)[value='+$(this).val()+']').remove(); - }); - var fromparent = $fromNode.parent(); - if (bindDblClick) { - //XXX jQuery live binding does not seem to work here - $newSelect.bind('dblclick', {'instanceData': {'fromNode':$fromNode.attr('id'), - 'toNode': $toNode.attr('id'), - 'cloned':clonedSelect, - 'bindDblClick':bindDblClick, - 'name': name}}, - methods.inOutWidgetAddValues); - } - $fromNode.remove(); - fromparent.append($newSelect); - }, + function sortoptions($optionlist) { + var $sorted = $optionlist.find('option').sort(function(opt1, opt2) { + return $(opt1).text() > $(opt2).text() ? 1 : -1; + }); + // this somehow translates to an inplace sort + $optionlist.append($sorted); + }; + sortoptions(state.$fromNode); + sortoptions(state.$toNode); - inOutWidgetAddValues: function(event){ - var $fromNode = $(cw.jqNode(event.data.instanceData.fromNode)); - var $toNode = $(cw.jqNode(event.data.instanceData.toNode)); - $fromNode.find('option:selected').each(function() { - var option = $(this); - var newoption = OPTION({'value':option.val()}, - value=option.text()); - $toNode.append(newoption); - var hiddenInput = INPUT({ - type: "hidden", name: event.data.instanceData.name, - value:option.val() + // will move selected options from one list to the other + // and call an option handler on each option + function moveoptions ($fromlist, $tolist, opthandler) { + $fromlist.find('option:selected').each(function(index, option) { + var $option = $(option); + // add a new option to the target list + $tolist.append(OPTION({'value' : $option.val()}, + $option.text())); + // process callback on the option + opthandler.call(null, $option); + // remove option from the source list + $option.remove(); }); - $toNode.parent().append(hiddenInput); - }); - methods.inOutWidgetRemplaceSelect($fromNode, $toNode, event.data.instanceData.cloned, - event.data.instanceData.bindDblClick, - event.data.instanceData.name); - // for ie 7 : ie does not resize correctly the select - if($.browser.msie && $.browser.version.substr(0,1) < 8){ - var p = $toNode.parent(); - var newtoNode = $toNode.clone(); - if (event.data.instanceData.bindDblClick) { - newtoNode.bind('dblclick', {'fromNode': $fromNode.attr('id'), - 'toNode': $toNode.attr('id'), - 'cloned': event.data.instanceData.cloned, - 'bindDblClick': true, - 'name': event.data.instanceData.name}, - methods.inOutWidgetRemoveValues); - } - $toNode.remove(); - p.append(newtoNode); - } - }, + // re-sort both lists + sortoptions($fromlist); + sortoptions($tolist); + }; + + function addvalues () { + moveoptions(state.$fromNode, state.$toNode, function ($option) { + // add an hidden input for the edit controller + var hiddenInput = INPUT({ + type: 'hidden', name: state.name, + value : $option.val() + }); + state.$toNode.parent().append(hiddenInput); + }); + }; - inOutWidgetRemoveValues: function(event){ - var $fromNode = $(cw.jqNode(event.data.instanceData.toNode)); - var $toNode = $(cw.jqNode(event.data.instanceData.fromNode)); - var name = event.data.instanceData.name.replace(':', '\\:'); - $fromNode.find('option:selected').each(function(){ - var option = $(this); - var newoption = OPTION({'value':option.val()}, - value=option.text()); - option.remove(); - $fromNode.parent().find('input[name]='+ name).each(function() { - $(this).val()==option.val()?$(this).remove():null; - }); - }); - methods.inOutWidgetRemplaceSelect($toNode, $fromNode, event.data.instanceData.cloned, - event.data.instanceData.bindDblClick, - event.data.instanceData.name); + function removevalues () { + moveoptions(state.$toNode, state.$fromNode, function($option) { + // remove hidden inputs for the edit controller + var selector = 'input[name=' + cw.escape(state.name) + ']' + state.$toNode.parent().find(selector).each(function(index, input) { + if ($(input).val() == $option.val()) { + $(input).remove(); + } + }); + }); + }; + + var $this = $(this); + $this.find('.cwinoutadd').bind( // 'add >>>' symbol + 'click', {'state' : state}, addvalues); + $this.find('.cwinoutremove').bind( // 'remove <<<' symbol + 'click', {'state' : state}, removevalues); + + state.$fromNode.bind('dblclick', {'state': state}, addvalues); + state.$toNode.bind('dblclick', {'state': state}, removevalues); + } }; - $.fn.cwinoutwidget = function(fromSelect, toSelect, options){ - return methods.__init__.apply(this, [fromSelect, toSelect, options]); + $.fn.cwinoutwidget = function(fromSelect, toSelect) { + return methods.__init__.apply(this, [fromSelect, toSelect]); }; -})(jQuery); \ No newline at end of file +})(jQuery); diff -r 2790fb8f7e03 -r e83cbc116352 web/data/entypo.eot Binary file web/data/entypo.eot has changed diff -r 2790fb8f7e03 -r e83cbc116352 web/data/entypo.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/entypo.svg Fri Jan 10 17:12:20 2014 +0100 @@ -0,0 +1,295 @@ + + + +Copyright (C) 2012 by Daniel Bruce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff -r 2790fb8f7e03 -r e83cbc116352 web/data/entypo.ttf Binary file web/data/entypo.ttf has changed diff -r 2790fb8f7e03 -r e83cbc116352 web/data/entypo.woff Binary file web/data/entypo.woff has changed diff -r 2790fb8f7e03 -r e83cbc116352 web/data/jquery-migrate.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/jquery-migrate.js Fri Jan 10 17:12:20 2014 +0100 @@ -0,0 +1,521 @@ +/*! + * jQuery Migrate - v1.2.1 - 2013-05-08 + * https://github.com/jquery/jquery-migrate + * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors; Licensed MIT + */ +(function( jQuery, window, undefined ) { +// See http://bugs.jquery.com/ticket/13335 +// "use strict"; + + +var warnedAbout = {}; + +// List of warnings already given; public read only +jQuery.migrateWarnings = []; + +// Set to true to prevent console output; migrateWarnings still maintained +// jQuery.migrateMute = false; + +// Show a message on the console so devs know we're active +if ( !jQuery.migrateMute && window.console && window.console.log ) { + window.console.log("JQMIGRATE: Logging is active"); +} + +// Set to false to disable traces that appear with warnings +if ( jQuery.migrateTrace === undefined ) { + jQuery.migrateTrace = true; +} + +// Forget any warnings we've already given; public +jQuery.migrateReset = function() { + warnedAbout = {}; + jQuery.migrateWarnings.length = 0; +}; + +function migrateWarn( msg) { + var console = window.console; + if ( !warnedAbout[ msg ] ) { + warnedAbout[ msg ] = true; + jQuery.migrateWarnings.push( msg ); + if ( console && console.warn && !jQuery.migrateMute ) { + console.warn( "JQMIGRATE: " + msg ); + if ( jQuery.migrateTrace && console.trace ) { + console.trace(); + } + } + } +} + +function migrateWarnProp( obj, prop, value, msg ) { + if ( Object.defineProperty ) { + // On ES5 browsers (non-oldIE), warn if the code tries to get prop; + // allow property to be overwritten in case some other plugin wants it + try { + Object.defineProperty( obj, prop, { + configurable: true, + enumerable: true, + get: function() { + migrateWarn( msg ); + return value; + }, + set: function( newValue ) { + migrateWarn( msg ); + value = newValue; + } + }); + return; + } catch( err ) { + // IE8 is a dope about Object.defineProperty, can't warn there + } + } + + // Non-ES5 (or broken) browser; just set the property + jQuery._definePropertyBroken = true; + obj[ prop ] = value; +} + +if ( document.compatMode === "BackCompat" ) { + // jQuery has never supported or tested Quirks Mode + migrateWarn( "jQuery is not compatible with Quirks Mode" ); +} + + +var attrFn = jQuery( "", { size: 1 } ).attr("size") && jQuery.attrFn, + oldAttr = jQuery.attr, + valueAttrGet = jQuery.attrHooks.value && jQuery.attrHooks.value.get || + function() { return null; }, + valueAttrSet = jQuery.attrHooks.value && jQuery.attrHooks.value.set || + function() { return undefined; }, + rnoType = /^(?:input|button)$/i, + rnoAttrNodeType = /^[238]$/, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + ruseDefault = /^(?:checked|selected)$/i; + +// jQuery.attrFn +migrateWarnProp( jQuery, "attrFn", attrFn || {}, "jQuery.attrFn is deprecated" ); + +jQuery.attr = function( elem, name, value, pass ) { + var lowerName = name.toLowerCase(), + nType = elem && elem.nodeType; + + if ( pass ) { + // Since pass is used internally, we only warn for new jQuery + // versions where there isn't a pass arg in the formal params + if ( oldAttr.length < 4 ) { + migrateWarn("jQuery.fn.attr( props, pass ) is deprecated"); + } + if ( elem && !rnoAttrNodeType.test( nType ) && + (attrFn ? name in attrFn : jQuery.isFunction(jQuery.fn[name])) ) { + return jQuery( elem )[ name ]( value ); + } + } + + // Warn if user tries to set `type`, since it breaks on IE 6/7/8; by checking + // for disconnected elements we don't warn on $( "