# HG changeset patch # User Julien Cristau # Date 1390313476 -3600 # Node ID 6880674c1a2669e3635abd688755116dda72e65e # Parent 549c999d06d27cbcbe8a4e2df0f8e4365886c2e7# Parent 1910d86afcbcb1a82d825688fd2c43dec168cff1 merge 3.17.12 into 3.18 branch diff -r 1910d86afcbc -r 6880674c1a26 .hgtags --- a/.hgtags Tue Jan 21 14:56:06 2014 +0100 +++ b/.hgtags Tue Jan 21 15:11:16 2014 +0100 @@ -323,3 +323,9 @@ 838d58a30f7efc6a8f83ac27ae8de7d79b84b2bb cubicweb-version-3.17.12 838d58a30f7efc6a8f83ac27ae8de7d79b84b2bb cubicweb-debian-version-3.17.12-1 838d58a30f7efc6a8f83ac27ae8de7d79b84b2bb cubicweb-centos-version-3.17.12-1 +db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-version-3.18.0 +db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-debian-version-3.18.0-1 +db37bf35a1474843ded0a537f9cb4838f4a78cda cubicweb-centos-version-3.18.0-1 +60322cb8636c0402cdac025d3297626c41583023 cubicweb-version-3.18.1 +60322cb8636c0402cdac025d3297626c41583023 cubicweb-debian-version-3.18.1-1 +60322cb8636c0402cdac025d3297626c41583023 cubicweb-centos-version-3.18.1-1 diff -r 1910d86afcbc -r 6880674c1a26 README --- a/README Tue Jan 21 14:56:06 2014 +0100 +++ b/README Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 __init__.py --- a/__init__.py Tue Jan 21 14:56:06 2014 +0100 +++ b/__init__.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 __pkginfo__.py --- a/__pkginfo__.py Tue Jan 21 14:56:06 2014 +0100 +++ b/__pkginfo__.py Tue Jan 21 15:11:16 2014 +0100 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 17, 12) +numversion = (3, 18, 2) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -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 1910d86afcbc -r 6880674c1a26 _exceptions.py --- a/_exceptions.py Tue Jan 21 14:56:06 2014 +0100 +++ b/_exceptions.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 cubicweb.spec --- a/cubicweb.spec Tue Jan 21 14:56:06 2014 +0100 +++ b/cubicweb.spec Tue Jan 21 15:11:16 2014 +0100 @@ -7,7 +7,7 @@ %endif Name: cubicweb -Version: 3.17.12 +Version: 3.18.2 Release: logilab.1%{?dist} Summary: CubicWeb is a semantic web application framework Source0: http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz @@ -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 1910d86afcbc -r 6880674c1a26 cwconfig.py --- a/cwconfig.py Tue Jan 21 14:56:06 2014 +0100 +++ b/cwconfig.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 cwctl.py --- a/cwctl.py Tue Jan 21 14:56:06 2014 +0100 +++ b/cwctl.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 cwvreg.py --- a/cwvreg.py Tue Jan 21 14:56:06 2014 +0100 +++ b/cwvreg.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 dataimport.py --- a/dataimport.py Tue Jan 21 14:56:06 2014 +0100 +++ b/dataimport.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 dbapi.py --- a/dbapi.py Tue Jan 21 14:56:06 2014 +0100 +++ b/dbapi.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 debian/changelog --- a/debian/changelog Tue Jan 21 14:56:06 2014 +0100 +++ b/debian/changelog Tue Jan 21 15:11:16 2014 +0100 @@ -1,3 +1,21 @@ +cubicweb (3.18.2-1) unstable; urgency=low + + * new upstream release. + + -- Julien Cristau Tue, 21 Jan 2014 12:34:00 +0100 + +cubicweb (3.18.1-1) unstable; urgency=low + + * New upstream release. + + -- Julien Cristau Thu, 16 Jan 2014 11:53:21 +0100 + +cubicweb (3.18.0-1) unstable; urgency=low + + * new upstream release. + + -- Julien Cristau Fri, 10 Jan 2014 17:14:18 +0100 + cubicweb (3.17.12-1) unstable; urgency=low * new upstream release diff -r 1910d86afcbc -r 6880674c1a26 debian/control --- a/debian/control Tue Jan 21 14:56:06 2014 +0100 +++ b/debian/control Tue Jan 21 15:11:16 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 @@ -52,7 +52,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, @@ -132,6 +132,8 @@ python-fyzz, python-imaging, python-rdflib +Breaks: + cubicweb-inlinedit (<< 1.1.1), Description: web interface library for the CubicWeb framework CubicWeb is a semantic web application framework. . @@ -150,9 +152,8 @@ graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), - python-logilab-common (>= 0.59.0), - python-yams (>= 0.37.0), - python-yams (<< 0.39), + python-logilab-common (>= 0.60.0), + python-yams (>= 0.39.0), python-rql (>= 0.31.2), python-lxml Recommends: @@ -160,6 +161,11 @@ python-crypto Conflicts: cubicweb-core Replaces: cubicweb-core +Breaks: + cubicweb-comment (<< 1.9.1), + cubicweb-person (<< 1.8.0), + cubicweb-geocoding (<< 0.2.0), + cubicweb-invoice (<< 0.6.1), Description: common library for the CubicWeb framework CubicWeb is a semantic web application framework. . diff -r 1910d86afcbc -r 6880674c1a26 debian/copyright --- a/debian/copyright Tue Jan 21 14:56:06 2014 +0100 +++ b/debian/copyright Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 devtools/__init__.py --- a/devtools/__init__.py Tue Jan 21 14:56:06 2014 +0100 +++ b/devtools/__init__.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 devtools/devctl.py --- a/devtools/devctl.py Tue Jan 21 14:56:06 2014 +0100 +++ b/devtools/devctl.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 devtools/instrument.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/devtools/instrument.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 devtools/testlib.py --- a/devtools/testlib.py Tue Jan 21 14:56:06 2014 +0100 +++ b/devtools/testlib.py Tue Jan 21 15:11:16 2014 +0100 @@ -111,7 +111,7 @@ MAILBOX = [] -class Email: +class Email(object): """you'll get instances of Email into MAILBOX during tests that trigger some notification. @@ -120,7 +120,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 @@ -147,8 +148,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 @@ -441,13 +442,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) @@ -469,10 +467,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) @@ -1005,15 +1000,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 1910d86afcbc -r 6880674c1a26 doc/3.15.rst --- a/doc/3.15.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/3.15.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/3.18.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/3.18.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/book/en/admin/config.rst --- a/doc/book/en/admin/config.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/book/en/admin/config.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/book/en/admin/setup.rst --- a/doc/book/en/admin/setup.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/book/en/admin/setup.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/book/en/devrepo/datamodel/definition.rst --- a/doc/book/en/devrepo/datamodel/definition.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/book/en/devrepo/datamodel/definition.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/book/en/devrepo/entityclasses/adapters.rst --- a/doc/book/en/devrepo/entityclasses/adapters.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/book/en/devrepo/entityclasses/adapters.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/book/en/devrepo/migration.rst --- a/doc/book/en/devrepo/migration.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/book/en/devrepo/migration.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/book/en/devrepo/testing.rst --- a/doc/book/en/devrepo/testing.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/book/en/devrepo/testing.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/book/en/devrepo/vreg.rst --- a/doc/book/en/devrepo/vreg.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/book/en/devrepo/vreg.rst Tue Jan 21 15:11:16 2014 +0100 @@ -81,7 +81,6 @@ .. autoclass:: cubicweb.predicates.has_mimetype .. autoclass:: cubicweb.predicates.is_in_state .. autofunction:: cubicweb.predicates.on_fire_transition -.. autoclass:: cubicweb.predicates.implements Logged user predicates diff -r 1910d86afcbc -r 6880674c1a26 doc/book/en/tutorials/advanced/part02_security.rst --- a/doc/book/en/tutorials/advanced/part02_security.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/book/en/tutorials/advanced/part02_security.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/book/en/tutorials/advanced/part04_ui-base.rst --- a/doc/book/en/tutorials/advanced/part04_ui-base.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/book/en/tutorials/advanced/part05_ui-advanced.rst --- a/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 doc/tools/pyjsrest.py --- a/doc/tools/pyjsrest.py Tue Jan 21 14:56:06 2014 +0100 +++ b/doc/tools/pyjsrest.py Tue Jan 21 15:11:16 2014 +0100 @@ -136,7 +136,6 @@ 'cubicweb.preferences', 'cubicweb.edition', 'cubicweb.reledit', - 'cubicweb.rhythm', 'cubicweb.timeline-ext', ] diff -r 1910d86afcbc -r 6880674c1a26 entities/adapters.py --- a/entities/adapters.py Tue Jan 21 14:56:06 2014 +0100 +++ b/entities/adapters.py Tue Jan 21 15:11:16 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. @@ -166,27 +162,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 @@ -218,27 +212,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. @@ -252,7 +235,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. @@ -266,23 +248,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). @@ -293,7 +271,6 @@ except (KeyError, IndexError): return None - @view.implements_adapter_compat('ITree') def children(self, entities=True, sametype=False): """Return children entities. @@ -306,7 +283,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): @@ -321,7 +297,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: @@ -333,7 +308,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: @@ -346,7 +320,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.""" @@ -388,23 +361,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 1910d86afcbc -r 6880674c1a26 entities/test/unittest_base.py --- a/entities/test/unittest_base.py Tue Jan 21 14:56:06 2014 +0100 +++ b/entities/test/unittest_base.py Tue Jan 21 15:11:16 2014 +0100 @@ -25,7 +25,6 @@ from cubicweb.devtools.testlib import CubicWebTC -from cubicweb.interfaces import IMileStone, ICalendarable from cubicweb.entities import AnyEntity @@ -155,27 +154,6 @@ self.assertNotIn('alert', c.printable_value('description', format='text/html')) -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 1910d86afcbc -r 6880674c1a26 entities/wfobjs.py --- a/entities/wfobjs.py Tue Jan 21 14:56:06 2014 +0100 +++ b/entities/wfobjs.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 entity.py --- a/entity.py Tue Jan 21 14:56:06 2014 +0100 +++ b/entity.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 etwist/request.py --- a/etwist/request.py Tue Jan 21 14:56:06 2014 +0100 +++ b/etwist/request.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 etwist/server.py --- a/etwist/server.py Tue Jan 21 14:56:06 2014 +0100 +++ b/etwist/server.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 etwist/twconfig.py --- a/etwist/twconfig.py Tue Jan 21 14:56:06 2014 +0100 +++ b/etwist/twconfig.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 etwist/twctl.py --- a/etwist/twctl.py Tue Jan 21 14:56:06 2014 +0100 +++ b/etwist/twctl.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 ext/rest.py --- a/ext/rest.py Tue Jan 21 14:56:06 2014 +0100 +++ b/ext/rest.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 ext/test/data/views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ext/test/data/views.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 ext/test/unittest_rest.py --- a/ext/test/unittest_rest.py Tue Jan 21 14:56:06 2014 +0100 +++ b/ext/test/unittest_rest.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 hooks/integrity.py --- a/hooks/integrity.py Tue Jan 21 14:56:06 2014 +0100 +++ b/hooks/integrity.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 hooks/metadata.py --- a/hooks/metadata.py Tue Jan 21 14:56:06 2014 +0100 +++ b/hooks/metadata.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 hooks/security.py --- a/hooks/security.py Tue Jan 21 14:56:06 2014 +0100 +++ b/hooks/security.py Tue Jan 21 15:11:16 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 @@ -41,29 +42,28 @@ for attr in editedattrs: if attr in dontcheck: continue - rdef = eschema.rdef(attr) + rdef = eschema.rdef(attr, takefirst=True) 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; as an optimization, avoid + # going through check_perm. + raise Unauthorized(action, str(rdef)) + rdef.check_perm(session, action, eid=eid) class CheckEntityPermissionOp(hook.DataOperationMixIn, hook.LateOperation): @@ -72,8 +72,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 +110,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 1910d86afcbc -r 6880674c1a26 hooks/syncschema.py --- a/hooks/syncschema.py Tue Jan 21 14:56:06 2014 +0100 +++ b/hooks/syncschema.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 hooks/test/data/schema.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hooks/test/data/schema.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 hooks/test/unittest_hooks.py --- a/hooks/test/unittest_hooks.py Tue Jan 21 14:56:06 2014 +0100 +++ b/hooks/test/unittest_hooks.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 hooks/test/unittest_syncschema.py --- a/hooks/test/unittest_syncschema.py Tue Jan 21 14:56:06 2014 +0100 +++ b/hooks/test/unittest_syncschema.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 i18n/de.po --- a/i18n/de.po Tue Jan 21 14:56:06 2014 +0100 +++ b/i18n/de.po Tue Jan 21 15:11:16 2014 +0100 @@ -145,9 +145,6 @@ msgid "(UNEXISTANT EID)" msgstr "(EID nicht gefunden)" -msgid "(not all shown here) " -msgstr "" - #, python-format msgid "(suppressed) entity #%d" msgstr "" @@ -442,10 +439,6 @@ msgid "DEBUG" msgstr "" -#, python-format -msgid "Data connection graph for %s" -msgstr "Graf der Datenverbindungen für %s" - msgid "Date" msgstr "Datum" @@ -524,9 +517,6 @@ msgid "Garbage collection information" msgstr "Information zur Speicherbereinigung" -msgid "Got rhythm?" -msgstr "Hast Du Rhythmus ?" - msgid "Help" msgstr "Hilfe" @@ -1032,6 +1022,9 @@ msgid "add Bookmark bookmarked_by CWUser object" msgstr "Lesezeichen" +msgid "add CWAttribute add_permission RQLExpression subject" +msgstr "" + msgid "add CWAttribute constrained_by CWConstraint subject" msgstr "Einschränkung" @@ -1140,6 +1133,10 @@ msgid "add_permission" msgstr "kann hinzugefügt werden durch" +msgctxt "CWAttribute" +msgid "add_permission" +msgstr "" + # subject and object forms for each relation type # (no object form for final relation types) msgctxt "CWEType" @@ -1718,6 +1715,10 @@ msgstr "Erstelle E-Mail-Adresse für Nutzer %(linkto)s" msgid "" +"creating RQLExpression (CWAttribute %(linkto)s add_permission RQLExpression)" +msgstr "" + +msgid "" "creating RQLExpression (CWAttribute %(linkto)s read_permission RQLExpression)" msgstr "RQL-Ausdruck für Leseberechtigung für %(linkto)s" @@ -2090,6 +2091,9 @@ msgid "default value" msgstr "Standardwert" +msgid "default value as gziped pickled python object" +msgstr "" + msgid "default workflow for an entity type" msgstr "Standard-Workflow eines Entitätstyps" @@ -3212,6 +3216,10 @@ msgid "name" msgstr "" +msgctxt "CWUniqueTogetherConstraint" +msgid "name" +msgstr "" + msgctxt "State" msgid "name" msgstr "Name" @@ -3813,8 +3821,7 @@ "Eine oder mehrere frühere Transaktion(en) betreffen die Tntität. Machen Sie " "sie zuerst rückgängig." -#, python-format -msgid "some relations %sviolate a unicity constraint" +msgid "some relations violate a unicity constraint" msgstr "" msgid "sorry, the server is unable to handle this query" @@ -4605,9 +4612,15 @@ #~ "Kann die Relation %(rtype)s der Entität %(eid)s nicht wieder herstellen, " #~ "diese Relation existiert nicht mehr in dem Schema." +#~ msgid "Data connection graph for %s" +#~ msgstr "Graf der Datenverbindungen für %s" + #~ msgid "From:" #~ msgstr "Von:" +#~ msgid "Got rhythm?" +#~ msgstr "Hast Du Rhythmus ?" + #~ msgid "Recipients:" #~ msgstr "Adressaten:" diff -r 1910d86afcbc -r 6880674c1a26 i18n/en.po --- a/i18n/en.po Tue Jan 21 14:56:06 2014 +0100 +++ b/i18n/en.po Tue Jan 21 15:11:16 2014 +0100 @@ -137,9 +137,6 @@ msgid "(UNEXISTANT EID)" msgstr "" -msgid "(not all shown here) " -msgstr "" - #, python-format msgid "(suppressed) entity #%d" msgstr "" @@ -420,10 +417,6 @@ msgid "DEBUG" msgstr "" -#, python-format -msgid "Data connection graph for %s" -msgstr "" - msgid "Date" msgstr "Date" @@ -502,9 +495,6 @@ msgid "Garbage collection information" msgstr "" -msgid "Got rhythm?" -msgstr "" - msgid "Help" msgstr "" @@ -994,6 +984,9 @@ msgid "add Bookmark bookmarked_by CWUser object" msgstr "bookmark" +msgid "add CWAttribute add_permission RQLExpression subject" +msgstr "rql expression for add permission" + msgid "add CWAttribute constrained_by CWConstraint subject" msgstr "constraint" @@ -1102,6 +1095,10 @@ msgid "add_permission" msgstr "can be added by" +msgctxt "CWAttribute" +msgid "add_permission" +msgstr "add permission" + # subject and object forms for each relation type # (no object form for final relation types) msgctxt "CWEType" @@ -1673,6 +1670,10 @@ msgstr "creating email address for user %(linkto)s" msgid "" +"creating RQLExpression (CWAttribute %(linkto)s add_permission RQLExpression)" +msgstr "RQL expression granting add permission on %(linkto)s" + +msgid "" "creating RQLExpression (CWAttribute %(linkto)s read_permission RQLExpression)" msgstr "RQL expression granting read permission on %(linkto)s" @@ -2047,6 +2048,9 @@ msgid "default value" msgstr "" +msgid "default value as gziped pickled python object" +msgstr "" + msgid "default workflow for an entity type" msgstr "" @@ -3130,6 +3134,10 @@ msgid "name" msgstr "name" +msgctxt "CWUniqueTogetherConstraint" +msgid "name" +msgstr "" + msgctxt "State" msgid "name" msgstr "name" @@ -3718,8 +3726,7 @@ msgid "some later transaction(s) touch entity, undo them first" msgstr "" -#, python-format -msgid "some relations %sviolate a unicity constraint" +msgid "some relations violate a unicity constraint" msgstr "" msgid "sorry, the server is unable to handle this query" diff -r 1910d86afcbc -r 6880674c1a26 i18n/es.po --- a/i18n/es.po Tue Jan 21 14:56:06 2014 +0100 +++ b/i18n/es.po Tue Jan 21 15:11:16 2014 +0100 @@ -146,9 +146,6 @@ msgid "(UNEXISTANT EID)" msgstr "(EID INEXISTENTE" -msgid "(not all shown here) " -msgstr "" - #, python-format msgid "(suppressed) entity #%d" msgstr "" @@ -442,10 +439,6 @@ msgid "DEBUG" msgstr "" -#, python-format -msgid "Data connection graph for %s" -msgstr "Gráfica de conexión de datos para %s" - msgid "Date" msgstr "Fecha" @@ -524,9 +517,6 @@ msgid "Garbage collection information" msgstr "Recolector de basura en memoria" -msgid "Got rhythm?" -msgstr "Tenemos Ritmo?" - msgid "Help" msgstr "Ayuda" @@ -1040,6 +1030,9 @@ msgid "add Bookmark bookmarked_by CWUser object" msgstr "Agregar a los favoritos " +msgid "add CWAttribute add_permission RQLExpression subject" +msgstr "" + msgid "add CWAttribute constrained_by CWConstraint subject" msgstr "Restricción" @@ -1148,6 +1141,10 @@ msgid "add_permission" msgstr "Autorización para agregar" +msgctxt "CWAttribute" +msgid "add_permission" +msgstr "" + # subject and object forms for each relation type # (no object form for final relation types) msgctxt "CWEType" @@ -1736,6 +1733,10 @@ msgstr "Creación de una dirección electrónica para el usuario %(linkto)s" msgid "" +"creating RQLExpression (CWAttribute %(linkto)s add_permission RQLExpression)" +msgstr "" + +msgid "" "creating RQLExpression (CWAttribute %(linkto)s read_permission RQLExpression)" msgstr "creación de una expresión RQL por el derecho de lectura de %(linkto)s" @@ -2118,6 +2119,9 @@ msgid "default value" msgstr "Valor por defecto" +msgid "default value as gziped pickled python object" +msgstr "" + msgid "default workflow for an entity type" msgstr "Workflow por defecto para un tipo de entidad" @@ -3251,6 +3255,10 @@ msgid "name" msgstr "" +msgctxt "CWUniqueTogetherConstraint" +msgid "name" +msgstr "" + msgctxt "State" msgid "name" msgstr "nombre" @@ -3859,8 +3867,7 @@ msgstr "" "Las transacciones más recientes modificaron esta entidad, anúlelas primero" -#, python-format -msgid "some relations %sviolate a unicity constraint" +msgid "some relations violate a unicity constraint" msgstr "" msgid "sorry, the server is unable to handle this query" @@ -4654,9 +4661,15 @@ #~ "No puede restaurar la relación %(rtype)s de la entidad %(eid)s, esta " #~ "relación ya no existe en el esquema." +#~ msgid "Data connection graph for %s" +#~ msgstr "Gráfica de conexión de datos para %s" + #~ msgid "From:" #~ msgstr "De: " +#~ msgid "Got rhythm?" +#~ msgstr "Tenemos Ritmo?" + #~ msgid "Recipients:" #~ msgstr "Destinatarios :" diff -r 1910d86afcbc -r 6880674c1a26 i18n/fr.po --- a/i18n/fr.po Tue Jan 21 14:56:06 2014 +0100 +++ b/i18n/fr.po Tue Jan 21 15:11:16 2014 +0100 @@ -148,9 +148,6 @@ msgid "(UNEXISTANT EID)" msgstr "(EID INTROUVABLE)" -msgid "(not all shown here) " -msgstr "(toutes ne sont pas montrées)" - #, python-format msgid "(suppressed) entity #%d" msgstr "entité #%d (supprimée)" @@ -445,10 +442,6 @@ msgid "DEBUG" msgstr "DEBUG" -#, python-format -msgid "Data connection graph for %s" -msgstr "Graphique de connection des données pour %s" - msgid "Date" msgstr "Date" @@ -527,9 +520,6 @@ msgid "Garbage collection information" msgstr "Information sur le ramasse-miette" -msgid "Got rhythm?" -msgstr "T'as le rythme ?" - msgid "Help" msgstr "Aide" @@ -1045,6 +1035,9 @@ msgid "add Bookmark bookmarked_by CWUser object" msgstr "signet" +msgid "add CWAttribute add_permission RQLExpression subject" +msgstr "définir une expression RQL d'ajout" + msgid "add CWAttribute constrained_by CWConstraint subject" msgstr "contrainte" @@ -1153,6 +1146,10 @@ msgid "add_permission" msgstr "peut ajouter" +msgctxt "CWAttribute" +msgid "add_permission" +msgstr "permission d'ajout" + # subject and object forms for each relation type # (no object form for final relation types) msgctxt "CWEType" @@ -1750,6 +1747,10 @@ msgstr "création d'une adresse électronique pour l'utilisateur %(linkto)s" msgid "" +"creating RQLExpression (CWAttribute %(linkto)s add_permission RQLExpression)" +msgstr "création d'une expression rql pour le droit d'ajout de %(linkto)s" + +msgid "" "creating RQLExpression (CWAttribute %(linkto)s read_permission RQLExpression)" msgstr "création d'une expression rql pour le droit de lecture de %(linkto)s" @@ -2134,6 +2135,9 @@ msgid "default value" msgstr "valeur par défaut" +msgid "default value as gziped pickled python object" +msgstr "valeur par défaut, sous forme d'objet python picklé zippé" + msgid "default workflow for an entity type" msgstr "workflow par défaut pour un type d'entité" @@ -3264,6 +3268,10 @@ msgid "name" msgstr "nom" +msgctxt "CWUniqueTogetherConstraint" +msgid "name" +msgstr "nom" + msgctxt "State" msgid "name" msgstr "nom" @@ -3874,9 +3882,8 @@ msgstr "" "des transactions plus récentes modifient cette entité, annulez les d'abord" -#, python-format -msgid "some relations %sviolate a unicity constraint" -msgstr "certaines relations %stransgressent une contrainte d'unicité" +msgid "some relations violate a unicity constraint" +msgstr "certaines relations transgressent une contrainte d'unicité" msgid "sorry, the server is unable to handle this query" msgstr "désolé, le serveur ne peut traiter cette requête" @@ -4659,107 +4666,3 @@ msgstr "" "vous devriez enlevé la mise en ligne de la relation %s qui est supportée et " "peut-être croisée" - -#~ msgid "%s not estimated" -#~ msgstr "%s non estimé(s)" - -#~ msgid "Action" -#~ msgstr "Action" - -#~ msgid "From:" -#~ msgstr "De :" - -#~ msgid "Recipients:" -#~ msgstr "Destinataires :" - -#~ msgid "Subject:" -#~ msgstr "Sujet :" - -#~ msgid "What's new?" -#~ msgstr "Nouveautés" - -#~ msgid "You can use any of the following substitutions in your text" -#~ msgstr "" -#~ "Vous pouvez utiliser n'importe quelle substitution parmi la liste " -#~ "suivante dans le contenu de votre courriel." - -#~ msgid "cost" -#~ msgstr "coût" - -#~ msgid "day" -#~ msgstr "jour" - -#~ msgid "emails successfully sent" -#~ msgstr "courriels envoyés avec succès" - -#~ msgid "embed" -#~ msgstr "embarqué" - -#~ msgid "embedding this url is forbidden" -#~ msgstr "l'inclusion de cette url est interdite" - -#~ msgid "error while embedding page" -#~ msgstr "erreur pendant l'inclusion de la page" - -#~ msgid "eta_date" -#~ msgstr "date de fin" - -#~ msgid "expected:" -#~ msgstr "attendu :" - -#~ msgid "external page" -#~ msgstr "page externe" - -#~ msgid "initial estimation %s" -#~ msgstr "estimation initiale %s" - -#~ msgid "jump to selection" -#~ msgstr "afficher cette sélection" - -#~ msgid "log out first" -#~ msgstr "déconnecter vous d'abord" - -#~ msgid "milestone" -#~ msgstr "jalon" - -#~ msgid "month" -#~ msgstr "mois" - -#~ msgid "no related project" -#~ msgstr "pas de projet rattaché" - -#~ msgid "progress" -#~ msgstr "avancement" - -#~ msgid "progress bar" -#~ msgstr "barre d'avancement" - -#~ msgid "project" -#~ msgstr "projet" - -#~ msgid "send email" -#~ msgstr "envoyer un courriel" - -#~ msgid "sioc" -#~ msgstr "sioc" - -#~ msgid "task progression" -#~ msgstr "avancement de la tâche" - -#~ msgid "today" -#~ msgstr "aujourd'hui" - -#~ msgid "todo_by" -#~ msgstr "à faire par" - -#~ msgid "toggle filter" -#~ msgstr "afficher/masquer le filtre" - -#~ msgid "undo last change" -#~ msgstr "annuler dernier changement" - -#~ msgid "violates unique_together constraints (%s)" -#~ msgstr "violation de contrainte unique_together (%s)" - -#~ msgid "week" -#~ msgstr "semaine" diff -r 1910d86afcbc -r 6880674c1a26 interfaces.py --- a/interfaces.py Tue Jan 21 14:56:06 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,214 +0,0 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This file is part of CubicWeb. -# -# CubicWeb is free software: you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) -# any later version. -# -# CubicWeb is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with CubicWeb. If not, see . -"""Standard interfaces. Deprecated in favor of adapters. - -.. note:: - - The `implements` selector used to match not only entity classes but also their - interfaces. This will disappear in a future version. You should define an - adapter for that interface and use `adaptable('MyIFace')` selector on appobjects - that require that interface. - -""" -__docformat__ = "restructuredtext en" - -from logilab.common.interface import Interface - - -# XXX deprecates in favor of IProgressAdapter -class IProgress(Interface): - """something that has a cost, a state and a progression""" - - @property - def cost(self): - """the total cost""" - - @property - def done(self): - """what is already done""" - - @property - def todo(self): - """what remains to be done""" - - def progress_info(self): - """returns a dictionary describing progress/estimated cost of the - version. - - - mandatory keys are (''estimated', 'done', 'todo') - - - optional keys are ('notestimated', 'notestimatedcorrected', - 'estimatedcorrected') - - 'noestimated' and 'notestimatedcorrected' should default to 0 - 'estimatedcorrected' should default to 'estimated' - """ - - def finished(self): - """returns True if status is finished""" - - def in_progress(self): - """returns True if status is not finished""" - - def progress(self): - """returns the % progress of the task item""" - -# XXX deprecates in favor of IMileStoneAdapter -class IMileStone(IProgress): - """represents an ITask's item""" - - parent_type = None # specify main task's type - - def get_main_task(self): - """returns the main ITask entity""" - - def initial_prevision_date(self): - """returns the initial expected end of the milestone""" - - def eta_date(self): - """returns expected date of completion based on what remains - to be done - """ - - def completion_date(self): - """returns date on which the subtask has been completed""" - - def contractors(self): - """returns the list of persons supposed to work on this task""" - -# XXX deprecates in favor of IEmbedableAdapter -class IEmbedable(Interface): - """interface for embedable entities""" - - def embeded_url(self): - """embed action interface""" - -# XXX deprecates in favor of ICalendarViewsAdapter -class ICalendarViews(Interface): - """calendar views interface""" - def matching_dates(self, begin, end): - """ - :param begin: day considered as begin of the range (`DateTime`) - :param end: day considered as end of the range (`DateTime`) - - :return: - a list of dates (`DateTime`) in the range [`begin`, `end`] on which - this entity apply - """ - -# XXX deprecates in favor of ICalendarableAdapter -class ICalendarable(Interface): - """interface for items that do have a begin date 'start' and an end date 'stop' - """ - - @property - def start(self): - """return start date""" - - @property - def stop(self): - """return stop state""" - -# XXX deprecates in favor of ICalendarableAdapter -class ITimetableViews(Interface): - """timetable views interface""" - def timetable_date(self): - """XXX explain - - :return: date (`DateTime`) - """ - -# XXX deprecates in favor of IGeocodableAdapter -class IGeocodable(Interface): - """interface required by geocoding views such as gmap-view""" - - @property - def latitude(self): - """returns the latitude of the entity""" - - @property - def longitude(self): - """returns the longitude of the entity""" - - def marker_icon(self): - """returns the icon that should be used as the marker""" - - -# XXX deprecates in favor of IEmailableAdapter -class IFeed(Interface): - """interface for entities with rss flux""" - - def rss_feed_url(self): - """""" - -# XXX deprecates in favor of IDownloadableAdapter -class IDownloadable(Interface): - """interface for downloadable entities""" - - def download_url(self): # XXX not really part of this interface - """return an url to download entity's content""" - def download_content_type(self): - """return MIME type of the downloadable content""" - def download_encoding(self): - """return encoding of the downloadable content""" - def download_file_name(self): - """return file name of the downloadable content""" - def download_data(self): - """return actual data of the downloadable content""" - -# XXX deprecates in favor of IPrevNextAdapter -class IPrevNext(Interface): - """interface for entities which can be linked to a previous and/or next - entity - """ - - def next_entity(self): - """return the 'next' entity""" - def previous_entity(self): - """return the 'previous' entity""" - -# XXX deprecates in favor of IBreadCrumbsAdapter -class IBreadCrumbs(Interface): - - def breadcrumbs(self, view, recurs=False): - pass - -# XXX deprecates in favor of ITreeAdapter -class ITree(Interface): - - def parent(self): - """returns the parent entity""" - - def children(self): - """returns the item's children""" - - def children_rql(self): - """XXX returns RQL to get children""" - - def iterchildren(self): - """iterates over the item's children""" - - def is_leaf(self): - """returns true if this node as no child""" - - def is_root(self): - """returns true if this node has no parent""" - - def root(self): - """returns the root object""" - diff -r 1910d86afcbc -r 6880674c1a26 mail.py --- a/mail.py Tue Jan 21 14:56:06 2014 +0100 +++ b/mail.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 migration.py --- a/migration.py Tue Jan 21 14:56:06 2014 +0100 +++ b/migration.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 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 Tue Jan 21 15:11:16 2014 +0100 @@ -0,0 +1,141 @@ +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) + +add_relation_definition('CWAttribute', 'add_permission', 'CWGroup') +add_relation_definition('CWAttribute', 'add_permission', 'RQLExpression') + +# a bad defaultval in 3.13.8 schema was fixed in 3.13.9, but the migration was missed +rql('SET ATTR defaultval NULL WHERE ATTR from_entity E, E name "CWSource", ATTR relation_type T, T name "in_synchronization"') + +# the migration gets confused when we change rdefs out from under it. So +# explicitly remove this size constraint so it doesn't stick around and break +# things later. +rdefeid = schema['defaultval'].rdefs.values()[0].eid +rql('DELETE CWConstraint C WHERE C cstrtype T, T name "SizeConstraint", R constrained_by C, R eid %(eid)s', {'eid': rdefeid}) + +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 + if isinstance(default, Binary): + # partially migrated instance, try to be idempotent + return default + 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", NOT X to_entity B') + +schema['defaultval'].rdefs.values()[0].object = schema['Bytes'] + +commit() + +sync_schema_props_perms('defaultval') + +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() + + +# all attributes perms have to be refreshed ... +for rschema in schema.relations(): + if rschema.final: + sync_schema_props_perms(rschema.type, syncprops=False) diff -r 1910d86afcbc -r 6880674c1a26 misc/migration/3.18.2_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.18.2_Any.py Tue Jan 21 15:11:16 2014 +0100 @@ -0,0 +1,2 @@ +install_custom_sql_scripts() +commit() diff -r 1910d86afcbc -r 6880674c1a26 mixins.py --- a/mixins.py Tue Jan 21 14:56:06 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,308 +0,0 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This file is part of CubicWeb. -# -# CubicWeb is free software: you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) -# any later version. -# -# CubicWeb is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with CubicWeb. If not, see . -"""mixins of entity/views organized somewhat in a graph or tree structure""" -__docformat__ = "restructuredtext en" - -from itertools import chain - -from logilab.common.decorators import cached -from logilab.common.deprecation import deprecated, class_deprecated - -from cubicweb.predicates import implements -from cubicweb.interfaces import ITree - - -class TreeMixIn(object): - """base tree-mixin implementing the tree interface - - This mixin has to be inherited explicitly and configured using the - tree_attribute, parent_target and children_target class attribute to - benefit from this default implementation - """ - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] TreeMixIn is deprecated, use/override ITreeAdapter instead (%(cls)s)' - - tree_attribute = None - # XXX misnamed - parent_target = 'subject' - children_target = 'object' - - def different_type_children(self, entities=True): - """return children entities of different type as this entity. - - according to the `entities` parameter, return entity objects or the - equivalent result set - """ - res = self.related(self.tree_attribute, self.children_target, - entities=entities) - if entities: - return [e for e in res if e.e_schema != self.e_schema] - return res.filtered_rset(lambda x: x.e_schema != self.e_schema, self.cw_col) - - def same_type_children(self, entities=True): - """return children entities of the same type as this entity. - - according to the `entities` parameter, return entity objects or the - equivalent result set - """ - res = self.related(self.tree_attribute, self.children_target, - entities=entities) - if entities: - return [e for e in res if e.e_schema == self.e_schema] - return res.filtered_rset(lambda x: x.e_schema is self.e_schema, self.cw_col) - - def iterchildren(self, _done=None): - if _done is None: - _done = set() - for child in self.children(): - if child.eid in _done: - self.error('loop in %s tree: %s', self.__regid__.lower(), child) - continue - yield child - _done.add(child.eid) - - def prefixiter(self, _done=None): - if _done is None: - _done = set() - if self.eid in _done: - return - _done.add(self.eid) - yield self - for child in self.same_type_children(): - for entity in child.prefixiter(_done): - yield entity - - @cached - def path(self): - """returns the list of eids from the root object to this object""" - path = [] - parent = self - while parent: - if parent.eid in path: - self.error('loop in %s tree: %s', self.__regid__.lower(), parent) - break - path.append(parent.eid) - try: - # check we are not leaving the tree - if (parent.tree_attribute != self.tree_attribute or - parent.parent_target != self.parent_target): - break - parent = parent.parent() - except AttributeError: - break - - path.reverse() - return path - - def iterparents(self, strict=True): - def _uptoroot(self): - curr = self - while True: - curr = curr.parent() - if curr is None: - break - yield curr - if not strict: - return chain([self], _uptoroot(self)) - return _uptoroot(self) - - ## ITree interface ######################################################## - def parent(self): - """return the parent entity if any, else None (e.g. if we are on the - root - """ - try: - return self.related(self.tree_attribute, self.parent_target, - entities=True)[0] - except (KeyError, IndexError): - return None - - def children(self, entities=True, sametype=False): - """return children entities - - according to the `entities` parameter, return entity objects or the - equivalent result set - """ - if sametype: - return self.same_type_children(entities) - else: - return self.related(self.tree_attribute, self.children_target, - entities=entities) - - def children_rql(self): - return self.cw_related_rql(self.tree_attribute, self.children_target) - - def is_leaf(self): - return len(self.children()) == 0 - - def is_root(self): - return self.parent() is None - - def root(self): - """return the root object""" - return self._cw.entity_from_eid(self.path()[0]) - - -class EmailableMixIn(object): - """base mixin providing the default get_email() method used by - the massmailing view - - NOTE: The default implementation is based on the - primary_email / use_email scheme - """ - @deprecated("[3.9] use entity.cw_adapt_to('IEmailable').get_email()") - def get_email(self): - if getattr(self, 'primary_email', None): - return self.primary_email[0].address - if getattr(self, 'use_email', None): - return self.use_email[0].address - return None - - -"""pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity -classes which have the relation described by the dict's key. - -NOTE: pluggable mixins can't override any method of the 'explicit' user classes tree -(eg without plugged classes). This includes bases Entity and AnyEntity classes. -""" -MI_REL_TRIGGERS = { - ('primary_email', 'subject'): EmailableMixIn, - ('use_email', 'subject'): EmailableMixIn, - } - - -# XXX move to cubicweb.web.views.treeview once we delete usage from this file -def _done_init(done, view, row, col): - """handle an infinite recursion safety belt""" - if done is None: - done = set() - entity = view.cw_rset.get_entity(row, col) - if entity.eid in done: - msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % { - 'rel': entity.cw_adapt_to('ITree').tree_relation, - 'eid': entity.eid - } - return None, msg - done.add(entity.eid) - return done, entity - - -class TreeViewMixIn(object): - """a recursive tree view""" - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] TreeViewMixIn is deprecated, use/override BaseTreeView instead (%(cls)s)' - - __regid__ = 'tree' - __select__ = implements(ITree, warn=False) - item_vid = 'treeitem' - - def call(self, done=None, **kwargs): - if done is None: - done = set() - super(TreeViewMixIn, self).call(done=done, **kwargs) - - def cell_call(self, row, col=0, vid=None, done=None, maxlevel=None, **kwargs): - assert maxlevel is None or maxlevel > 0 - done, entity = _done_init(done, self, row, col) - if done is None: - # entity is actually an error message - self.w(u'
  • %s
  • ' % entity) - return - self.open_item(entity) - entity.view(vid or self.item_vid, w=self.w, **kwargs) - if maxlevel is not None: - maxlevel -= 1 - if maxlevel == 0: - self.close_item(entity) - return - relatedrset = entity.children(entities=False) - self.wview(self.__regid__, relatedrset, 'null', done=done, - maxlevel=maxlevel, **kwargs) - self.close_item(entity) - - def open_item(self, entity): - self.w(u'
  • \n' % entity.cw_etype.lower()) - def close_item(self, entity): - self.w(u'
  • \n') - - -class TreePathMixIn(object): - """a recursive path view""" - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] TreePathMixIn is deprecated, use/override TreePathView instead (%(cls)s)' - __regid__ = 'path' - item_vid = 'oneline' - separator = u' > ' - - def call(self, **kwargs): - self.w(u'
    ') - super(TreePathMixIn, self).call(**kwargs) - self.w(u'
    ') - - def cell_call(self, row, col=0, vid=None, done=None, **kwargs): - done, entity = _done_init(done, self, row, col) - if done is None: - # entity is actually an error message - self.w(u'%s' % entity) - return - parent = entity.parent() - if parent: - parent.view(self.__regid__, w=self.w, done=done) - self.w(self.separator) - entity.view(vid or self.item_vid, w=self.w) - - -class ProgressMixIn(object): - """provide a default implementations for IProgress interface methods""" - __metaclass__ = class_deprecated - __deprecation_warning__ = '[3.9] ProgressMixIn is deprecated, use/override IProgressAdapter instead (%(cls)s)' - - @property - def cost(self): - return self.progress_info()['estimated'] - - @property - def revised_cost(self): - return self.progress_info().get('estimatedcorrected', self.cost) - - @property - def done(self): - return self.progress_info()['done'] - - @property - def todo(self): - return self.progress_info()['todo'] - - @cached - def progress_info(self): - raise NotImplementedError() - - def finished(self): - return not self.in_progress() - - def in_progress(self): - raise NotImplementedError() - - def progress(self): - try: - return 100. * self.done / self.revised_cost - except ZeroDivisionError: - # total cost is 0 : if everything was estimated, task is completed - if self.progress_info().get('notestimated'): - return 0. - return 100 diff -r 1910d86afcbc -r 6880674c1a26 predicates.py --- a/predicates.py Tue Jan 21 14:56:06 2014 +0100 +++ b/predicates.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 req.py --- a/req.py Tue Jan 21 14:56:06 2014 +0100 +++ b/req.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 rqlrewrite.py --- a/rqlrewrite.py Tue Jan 21 14:56:06 2014 +0100 +++ b/rqlrewrite.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 rset.py --- a/rset.py Tue Jan 21 14:56:06 2014 +0100 +++ b/rset.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 schema.py --- a/schema.py Tue Jan 21 14:56:06 2014 +0100 +++ b/schema.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 schemas/_regproc.postgres.sql --- a/schemas/_regproc.postgres.sql Tue Jan 21 14:56:06 2014 +0100 +++ b/schemas/_regproc.postgres.sql Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 schemas/bootstrap.py --- a/schemas/bootstrap.py Tue Jan 21 14:56:06 2014 +0100 +++ b/schemas/bootstrap.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/__init__.py --- a/server/__init__.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/__init__.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/hook.py --- a/server/hook.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/hook.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/hooksmanager.py --- a/server/hooksmanager.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/hooksmanager.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/migractions.py --- a/server/migractions.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/migractions.py Tue Jan 21 15:11:16 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 @@ -395,11 +395,10 @@ sql_scripts = glob(osp.join(directory, '*.%s.sql' % driver)) for fpath in sql_scripts: print '-> installing', fpath - try: - sqlexec(open(fpath).read(), self.session.system_sql, False, - delimiter=';;') - except Exception as exc: - print '-> ERROR:', exc, ', skipping', fpath + failed = sqlexec(open(fpath).read(), self.session.system_sql, False, + delimiter=';;') + if failed: + print '-> ERROR, skipping', fpath # schema synchronization internals ######################################## @@ -559,39 +558,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 +1462,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 1910d86afcbc -r 6880674c1a26 server/msplanner.py --- a/server/msplanner.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/msplanner.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/querier.py --- a/server/querier.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/querier.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/repository.py --- a/server/repository.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/repository.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/rqlannotation.py --- a/server/rqlannotation.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/rqlannotation.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/schemaserial.py --- a/server/schemaserial.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/schemaserial.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/serverctl.py --- a/server/serverctl.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/serverctl.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/session.py --- a/server/session.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/session.py Tue Jan 21 15:11:16 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 @@ -1440,7 +1437,8 @@ self.user._cw = self # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone if not safe: self.disable_hook_categories('integrity') - self._tx.ctx_count += 1 + self.disable_hook_categories('security') + self._tx.ctx_count += 1 def __enter__(self): return self diff -r 1910d86afcbc -r 6880674c1a26 server/sources/__init__.py --- a/server/sources/__init__.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/sources/__init__.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/sources/ldapuser.py --- a/server/sources/ldapuser.py Tue Jan 21 14:56:06 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,551 +0,0 @@ -# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This file is part of CubicWeb. -# -# CubicWeb is free software: you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) -# any later version. -# -# CubicWeb is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with CubicWeb. If not, see . -"""cubicweb ldap user source - -this source is for now limited to a read-only CWUser source -""" -from __future__ import division, with_statement -from base64 import b64decode - -import ldap -from ldap.filter import escape_filter_chars - -from rql.nodes import Relation, VariableRef, Constant, Function - -import warnings -from cubicweb import UnknownEid, RepositoryError -from cubicweb.server import ldaputils -from cubicweb.server.utils import cartesian_product -from cubicweb.server.sources import (AbstractSource, TrFunc, GlobTrFunc, - TimedCache) - -# search scopes -BASE = ldap.SCOPE_BASE -ONELEVEL = ldap.SCOPE_ONELEVEL -SUBTREE = ldap.SCOPE_SUBTREE - -# map ldap protocol to their standard port -PROTO_PORT = {'ldap': 389, - 'ldaps': 636, - 'ldapi': None, - } - - -# module is lazily imported -warnings.warn('Imminent drop of ldapuser. Switch to ldapfeed now!', - DeprecationWarning) - - -class LDAPUserSource(ldaputils.LDAPSourceMixIn, AbstractSource): - """LDAP read-only CWUser source""" - support_entities = {'CWUser': False} - - options = ldaputils.LDAPSourceMixIn.options + ( - - ('synchronization-interval', - {'type' : 'time', - 'default': '1d', - 'help': 'interval between synchronization with the ldap \ -directory (default to once a day).', - 'group': 'ldap-source', 'level': 3, - }), - ('cache-life-time', - {'type' : 'time', - 'default': '2h', - 'help': 'life time of query cache (default to two hours).', - 'group': 'ldap-source', 'level': 3, - }), - - ) - - def update_config(self, source_entity, typedconfig): - """update configuration from source entity. `typedconfig` is config - properly typed with defaults set - """ - super(LDAPUserSource, self).update_config(source_entity, typedconfig) - self._interval = typedconfig['synchronization-interval'] - self._cache_ttl = max(71, typedconfig['cache-life-time']) - self.reset_caches() - # XXX copy from datafeed source - if source_entity is not None: - self._entity_update(source_entity) - self.config = typedconfig - # /end XXX - - def reset_caches(self): - """method called during test to reset potential source caches""" - self._cache = {} - self._query_cache = TimedCache(self._cache_ttl) - - def init(self, activated, source_entity): - """method called by the repository once ready to handle request""" - super(LDAPUserSource, self).init(activated, source_entity) - if activated: - self.info('ldap init') - # set minimum period of 5min 1s (the additional second is to - # minimize resonnance effet) - if self.user_rev_attrs['email']: - self.repo.looping_task(max(301, self._interval), self.synchronize) - self.repo.looping_task(self._cache_ttl // 10, - self._query_cache.clear_expired) - - def synchronize(self): - with self.repo.internal_session() as session: - self.pull_data(session) - - def pull_data(self, session, force=False, raise_on_error=False): - """synchronize content known by this repository with content in the - external repository - """ - self.info('synchronizing ldap source %s', self.uri) - ldap_emailattr = self.user_rev_attrs['email'] - assert ldap_emailattr - execute = session.execute - cursor = session.system_sql("SELECT eid, extid FROM entities WHERE " - "source='%s'" % self.uri) - for eid, b64extid in cursor.fetchall(): - extid = b64decode(b64extid) - self.debug('ldap eid %s', eid) - # if no result found, _search automatically delete entity information - res = self._search(session, extid, BASE) - self.debug('ldap search %s', res) - if res: - ldapemailaddr = res[0].get(ldap_emailattr) - if ldapemailaddr: - if isinstance(ldapemailaddr, list): - ldapemailaddr = ldapemailaddr[0] # XXX consider only the first email in the list - rset = execute('Any X,A WHERE ' - 'X address A, U use_email X, U eid %(u)s', - {'u': eid}) - ldapemailaddr = unicode(ldapemailaddr) - for emaileid, emailaddr, in rset: - if emailaddr == ldapemailaddr: - break - else: - self.debug('updating email address of user %s to %s', - extid, ldapemailaddr) - emailrset = execute('EmailAddress A WHERE A address %(addr)s', - {'addr': ldapemailaddr}) - if emailrset: - execute('SET U use_email X WHERE ' - 'X eid %(x)s, U eid %(u)s', - {'x': emailrset[0][0], 'u': eid}) - elif rset: - if not execute('SET X address %(addr)s WHERE ' - 'U primary_email X, U eid %(u)s', - {'addr': ldapemailaddr, 'u': eid}): - execute('SET X address %(addr)s WHERE ' - 'X eid %(x)s', - {'addr': ldapemailaddr, 'x': rset[0][0]}) - else: - # no email found, create it - _insert_email(session, ldapemailaddr, eid) - session.commit() - - def ldap_name(self, var): - if var.stinfo['relations']: - relname = iter(var.stinfo['relations']).next().r_type - return self.user_rev_attrs.get(relname) - return None - - def prepare_columns(self, mainvars, rqlst): - """return two list describing how to build the final results - from the result of an ldap search (ie a list of dictionary) - """ - columns = [] - global_transforms = [] - for i, term in enumerate(rqlst.selection): - if isinstance(term, Constant): - columns.append(term) - continue - if isinstance(term, Function): # LOWER, UPPER, COUNT... - var = term.get_nodes(VariableRef)[0] - var = var.variable - try: - mainvar = var.stinfo['attrvar'].name - except AttributeError: # no attrvar set - mainvar = var.name - assert mainvar in mainvars - trname = term.name - ldapname = self.ldap_name(var) - if trname in ('COUNT', 'MIN', 'MAX', 'SUM'): - global_transforms.append(GlobTrFunc(trname, i, ldapname)) - columns.append((mainvar, ldapname)) - continue - if trname in ('LOWER', 'UPPER'): - columns.append((mainvar, TrFunc(trname, i, ldapname))) - continue - raise NotImplementedError('no support for %s function' % trname) - if term.name in mainvars: - columns.append((term.name, 'dn')) - continue - var = term.variable - mainvar = var.stinfo['attrvar'].name - columns.append((mainvar, self.ldap_name(var))) - #else: - # # probably a bug in rql splitting if we arrive here - # raise NotImplementedError - return columns, global_transforms - - def syntax_tree_search(self, session, union, - args=None, cachekey=None, varmap=None, debug=0): - """return result from this source for a rql query (actually from a rql - syntax tree and a solution dictionary mapping each used variable to a - possible type). If cachekey is given, the query necessary to fetch the - results (but not the results themselves) may be cached using this key. - """ - self.debug('ldap syntax tree search') - # XXX not handled : transform/aggregat function, join on multiple users... - assert len(union.children) == 1, 'union not supported' - rqlst = union.children[0] - assert not rqlst.with_, 'subquery not supported' - rqlkey = rqlst.as_string(kwargs=args) - try: - results = self._query_cache[rqlkey] - except KeyError: - try: - results = self.rqlst_search(session, rqlst, args) - self._query_cache[rqlkey] = results - except ldap.SERVER_DOWN: - # cant connect to server - msg = session._("can't connect to source %s, some data may be missing") - session.set_shared_data('sources_error', msg % self.uri, txdata=True) - return [] - return results - - def rqlst_search(self, session, rqlst, args): - mainvars = [] - for varname in rqlst.defined_vars: - for sol in rqlst.solutions: - if sol[varname] == 'CWUser': - mainvars.append(varname) - break - assert mainvars, rqlst - columns, globtransforms = self.prepare_columns(mainvars, rqlst) - eidfilters = [lambda x: x > 0] - allresults = [] - generator = RQL2LDAPFilter(self, session, args, mainvars) - for mainvar in mainvars: - # handle restriction - try: - eidfilters_, ldapfilter = generator.generate(rqlst, mainvar) - except GotDN as ex: - assert ex.dn, 'no dn!' - try: - res = [self._cache[ex.dn]] - except KeyError: - res = self._search(session, ex.dn, BASE) - except UnknownEid as ex: - # raised when we are looking for the dn of an eid which is not - # coming from this source - res = [] - else: - eidfilters += eidfilters_ - res = self._search(session, self.user_base_dn, - self.user_base_scope, ldapfilter) - allresults.append(res) - # 1. get eid for each dn and filter according to that eid if necessary - for i, res in enumerate(allresults): - filteredres = [] - for resdict in res: - # get sure the entity exists in the system table - eid = self.repo.extid2eid(self, resdict['dn'], 'CWUser', session) - for eidfilter in eidfilters: - if not eidfilter(eid): - break - else: - resdict['eid'] = eid - filteredres.append(resdict) - allresults[i] = filteredres - # 2. merge result for each "mainvar": cartesian product - allresults = cartesian_product(allresults) - # 3. build final result according to column definition - result = [] - for rawline in allresults: - rawline = dict(zip(mainvars, rawline)) - line = [] - for varname, ldapname in columns: - if ldapname is None: - value = None # no mapping available - elif ldapname == 'dn': - value = rawline[varname]['eid'] - elif isinstance(ldapname, Constant): - if ldapname.type == 'Substitute': - value = args[ldapname.value] - else: - value = ldapname.value - elif isinstance(ldapname, TrFunc): - value = ldapname.apply(rawline[varname]) - else: - value = rawline[varname].get(ldapname) - line.append(value) - result.append(line) - for trfunc in globtransforms: - result = trfunc.apply(result) - #print '--> ldap result', result - return result - - def _process_ldap_item(self, dn, iterator): - itemdict = super(LDAPUserSource, self)._process_ldap_item(dn, iterator) - self._cache[dn] = itemdict - return itemdict - - def _process_no_such_object(self, session, dn): - eid = self.repo.extid2eid(self, dn, 'CWUser', session, insert=False) - if eid: - self.warning('deleting ldap user with eid %s and dn %s', eid, dn) - entity = session.entity_from_eid(eid, 'CWUser') - self.repo.delete_info(session, entity, self.uri) - self.reset_caches() - - def before_entity_insertion(self, session, lid, etype, eid, sourceparams): - """called by the repository when an eid has been attributed for an - entity stored here but the entity has not been inserted in the system - table yet. - - This method must return the an Entity instance representation of this - entity. - """ - self.debug('ldap before entity insertion') - entity = super(LDAPUserSource, self).before_entity_insertion( - session, lid, etype, eid, sourceparams) - res = self._search(session, lid, BASE)[0] - for attr in entity.e_schema.indexable_attributes(): - entity.cw_edited[attr] = res[self.user_rev_attrs[attr]] - return entity - - def after_entity_insertion(self, session, lid, entity, sourceparams): - """called by the repository after an entity stored here has been - inserted in the system table. - """ - self.debug('ldap after entity insertion') - super(LDAPUserSource, self).after_entity_insertion( - session, lid, entity, sourceparams) - for group in self.user_default_groups: - session.execute('SET X in_group G WHERE X eid %(x)s, G name %(group)s', - {'x': entity.eid, 'group': group}) - # search for existant email first - try: - # lid = dn - emailaddr = self._cache[lid][self.user_rev_attrs['email']] - except KeyError: - return - if isinstance(emailaddr, list): - emailaddr = emailaddr[0] # XXX consider only the first email in the list - rset = session.execute('EmailAddress X WHERE X address %(addr)s', - {'addr': emailaddr}) - if rset: - session.execute('SET U primary_email X WHERE U eid %(u)s, X eid %(x)s', - {'x': rset[0][0], 'u': entity.eid}) - else: - # not found, create it - _insert_email(session, emailaddr, entity.eid) - - def update_entity(self, session, entity): - """replace an entity in the source""" - raise RepositoryError('this source is read only') - - def delete_entity(self, session, entity): - """delete an entity from the source""" - raise RepositoryError('this source is read only') - - -def _insert_email(session, emailaddr, ueid): - session.execute('INSERT EmailAddress X: X address %(addr)s, U primary_email X ' - 'WHERE U eid %(x)s', {'addr': emailaddr, 'x': ueid}) - -class GotDN(Exception): - """exception used when a dn localizing the searched user has been found""" - def __init__(self, dn): - self.dn = dn - - -class RQL2LDAPFilter(object): - """generate an LDAP filter for a rql query""" - def __init__(self, source, session, args=None, mainvars=()): - self.source = source - self.repo = source.repo - self._ldap_attrs = source.user_rev_attrs - self._base_filters = source.base_filters - self._session = session - if args is None: - args = {} - self._args = args - self.mainvars = mainvars - - def generate(self, selection, mainvarname): - self._filters = res = self._base_filters[:] - self._mainvarname = mainvarname - self._eidfilters = [] - self._done_not = set() - restriction = selection.where - if isinstance(restriction, Relation): - # only a single relation, need to append result here (no AND/OR) - filter = restriction.accept(self) - if filter is not None: - res.append(filter) - elif restriction: - restriction.accept(self) - if len(res) > 1: - return self._eidfilters, '(&%s)' % ''.join(res) - return self._eidfilters, res[0] - - def visit_and(self, et): - """generate filter for a AND subtree""" - for c in et.children: - part = c.accept(self) - if part: - self._filters.append(part) - - def visit_or(self, ou): - """generate filter for a OR subtree""" - res = [] - for c in ou.children: - part = c.accept(self) - if part: - res.append(part) - if res: - if len(res) > 1: - part = '(|%s)' % ''.join(res) - else: - part = res[0] - self._filters.append(part) - - def visit_not(self, node): - """generate filter for a OR subtree""" - part = node.children[0].accept(self) - if part: - self._filters.append('(!(%s))'% part) - - def visit_relation(self, relation): - """generate filter for a relation""" - rtype = relation.r_type - # don't care of type constraint statement (i.e. relation_type = 'is') - if rtype == 'is': - return '' - lhs, rhs = relation.get_parts() - # attribute relation - if self.source.schema.rschema(rtype).final: - # dunno what to do here, don't pretend anything else - if lhs.name != self._mainvarname: - if lhs.name in self.mainvars: - # XXX check we don't have variable as rhs - return - raise NotImplementedError - rhs_vars = rhs.get_nodes(VariableRef) - if rhs_vars: - if len(rhs_vars) > 1: - raise NotImplementedError - # selected variable, nothing to do here - return - # no variables in the RHS - if isinstance(rhs.children[0], Function): - res = rhs.children[0].accept(self) - elif rtype != 'has_text': - res = self._visit_attribute_relation(relation) - else: - raise NotImplementedError(relation) - # regular relation XXX todo: in_group - else: - raise NotImplementedError(relation) - return res - - def _visit_attribute_relation(self, relation): - """generate filter for an attribute relation""" - lhs, rhs = relation.get_parts() - lhsvar = lhs.variable - if relation.r_type == 'eid': - # XXX hack - # skip comparison sign - eid = int(rhs.children[0].accept(self)) - if relation.neged(strict=True): - self._done_not.add(relation.parent) - self._eidfilters.append(lambda x: not x == eid) - return - if rhs.operator != '=': - filter = {'>': lambda x: x > eid, - '>=': lambda x: x >= eid, - '<': lambda x: x < eid, - '<=': lambda x: x <= eid, - }[rhs.operator] - self._eidfilters.append(filter) - return - dn = self.repo.eid2extid(self.source, eid, self._session) - raise GotDN(dn) - try: - filter = '(%s%s)' % (self._ldap_attrs[relation.r_type], - rhs.accept(self)) - except KeyError: - # unsupported attribute - self.source.warning('%s source can\'t handle relation %s, no ' - 'results will be returned from this source', - self.source.uri, relation) - raise UnknownEid # trick to return no result - return filter - - def visit_comparison(self, cmp): - """generate filter for a comparaison""" - return '%s%s'% (cmp.operator, cmp.children[0].accept(self)) - - def visit_mathexpression(self, mexpr): - """generate filter for a mathematic expression""" - raise NotImplementedError - - def visit_function(self, function): - """generate filter name for a function""" - if function.name == 'IN': - return self.visit_in(function) - raise NotImplementedError - - def visit_in(self, function): - grandpapa = function.parent.parent - ldapattr = self._ldap_attrs[grandpapa.r_type] - res = [] - for c in function.children: - part = c.accept(self) - if part: - res.append(part) - if res: - if len(res) > 1: - part = '(|%s)' % ''.join('(%s=%s)' % (ldapattr, v) for v in res) - else: - part = '(%s=%s)' % (ldapattr, res[0]) - return part - - def visit_constant(self, constant): - """generate filter name for a constant""" - value = constant.value - if constant.type is None: - raise NotImplementedError - if constant.type == 'Date': - raise NotImplementedError - #value = self.keyword_map[value]() - elif constant.type == 'Substitute': - value = self._args[constant.value] - else: - value = constant.value - if isinstance(value, unicode): - value = value.encode('utf8') - else: - value = str(value) - return escape_filter_chars(value) - - def visit_variableref(self, variableref): - """get the sql name for a variable reference""" - pass - diff -r 1910d86afcbc -r 6880674c1a26 server/sources/native.py --- a/server/sources/native.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/sources/native.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/sources/pyrorql.py --- a/server/sources/pyrorql.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/sources/pyrorql.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/sources/rql2sql.py --- a/server/sources/rql2sql.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/sources/rql2sql.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/sqlutils.py --- a/server/sqlutils.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/sqlutils.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/test/data/migratedapp/schema.py --- a/server/test/data/migratedapp/schema.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/test/data/migratedapp/schema.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/test/data/schema.py --- a/server/test/data/schema.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/test/data/schema.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/test/unittest_ldapsource.py --- a/server/test/unittest_ldapsource.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/test/unittest_ldapsource.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/test/unittest_migractions.py --- a/server/test/unittest_migractions.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/test/unittest_migractions.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/test/unittest_querier.py --- a/server/test/unittest_querier.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/test/unittest_querier.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/test/unittest_repository.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/test/unittest_rql2sql.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/test/unittest_schemaserial.py --- a/server/test/unittest_schemaserial.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/test/unittest_schemaserial.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/test/unittest_security.py --- a/server/test/unittest_security.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/test/unittest_security.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 server/test/unittest_session.py --- a/server/test/unittest_session.py Tue Jan 21 14:56:06 2014 +0100 +++ b/server/test/unittest_session.py Tue Jan 21 15:11:16 2014 +0100 @@ -28,11 +28,11 @@ def test_integrity_hooks(self): with self.repo.internal_session() as session: self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode) - self.assertEqual(set(('integrity',)), session.disabled_hook_categories) + self.assertEqual(set(('integrity', 'security')), session.disabled_hook_categories) self.assertEqual(set(), session.enabled_hook_categories) session.commit() self.assertEqual(HOOKS_ALLOW_ALL, session.hooks_mode) - self.assertEqual(set(('integrity',)), session.disabled_hook_categories) + self.assertEqual(set(('integrity', 'security')), session.disabled_hook_categories) self.assertEqual(set(), session.enabled_hook_categories) class SessionTC(CubicWebTC): diff -r 1910d86afcbc -r 6880674c1a26 skeleton/i18n/en.po --- a/skeleton/i18n/en.po Tue Jan 21 14:56:06 2014 +0100 +++ b/skeleton/i18n/en.po Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 skeleton/i18n/es.po --- a/skeleton/i18n/es.po Tue Jan 21 14:56:06 2014 +0100 +++ b/skeleton/i18n/es.po Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 skeleton/i18n/fr.po --- a/skeleton/i18n/fr.po Tue Jan 21 14:56:06 2014 +0100 +++ b/skeleton/i18n/fr.po Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 sobjects/notification.py --- a/sobjects/notification.py Tue Jan 21 14:56:06 2014 +0100 +++ b/sobjects/notification.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 test/data/migration/0.1.0_web.py --- a/test/data/migration/0.1.0_web.py Tue Jan 21 14:56:06 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This file is part of CubicWeb. -# -# CubicWeb is free software: you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) -# any later version. -# -# CubicWeb is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with CubicWeb. If not, see . -"""web only - -""" diff -r 1910d86afcbc -r 6880674c1a26 test/data/schema.py --- a/test/data/schema.py Tue Jan 21 14:56:06 2014 +0100 +++ b/test/data/schema.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 test/unittest_entity.py --- a/test/unittest_entity.py Tue Jan 21 14:56:06 2014 +0100 +++ b/test/unittest_entity.py Tue Jan 21 15:11:16 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() @@ -739,7 +757,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') @@ -753,6 +771,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 1910d86afcbc -r 6880674c1a26 test/unittest_mail.py --- a/test/unittest_mail.py Tue Jan 21 14:56:06 2014 +0100 +++ b/test/unittest_mail.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 test/unittest_migration.py --- a/test/unittest_migration.py Tue Jan 21 14:56:06 2014 +0100 +++ b/test/unittest_migration.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 test/unittest_req.py --- a/test/unittest_req.py Tue Jan 21 14:56:06 2014 +0100 +++ b/test/unittest_req.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 test/unittest_rqlrewrite.py --- a/test/unittest_rqlrewrite.py Tue Jan 21 14:56:06 2014 +0100 +++ b/test/unittest_rqlrewrite.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 test/unittest_rset.py --- a/test/unittest_rset.py Tue Jan 21 14:56:06 2014 +0100 +++ b/test/unittest_rset.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 view.py --- a/view.py Tue Jan 21 14:56:06 2014 +0100 +++ b/view.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/__init__.py --- a/web/__init__.py Tue Jan 21 14:56:06 2014 +0100 +++ b/web/__init__.py Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.ajax.box.js --- a/web/data/cubicweb.ajax.box.js Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.ajax.box.js Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.ajax.js --- a/web/data/cubicweb.ajax.js Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.ajax.js Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.calendar.css --- a/web/data/cubicweb.calendar.css Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.calendar.css Tue Jan 21 15:11:16 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{ @@ -340,21 +342,21 @@ /* ------------------------- */ /* tooltips for fullcalendar */ -div.calevent div.tooltip { +a.calevent div.tooltip { display: none; /* tooltip hidden */ } -div.calevent:hover { +a.calevent:hover { z-index: auto !important; /* in order that the tooltip from the above .calevent div can be put over this div*/ } -div.calevent a{ +a.calevent { display: inline; font-size: none; font-weight: bold; } -div.calevent:hover div.tooltip{ +a.calevent:hover div.tooltip{ display: block; position: absolute; z-index: 10; @@ -380,4 +382,4 @@ div.fc-view{ overflow: visible; -} \ No newline at end of file +} diff -r 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.compat.js --- a/web/data/cubicweb.compat.js Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.compat.js Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.css --- a/web/data/cubicweb.css Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.css Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.facets.css --- a/web/data/cubicweb.facets.css Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.facets.css Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.facets.js --- a/web/data/cubicweb.facets.js Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.facets.js Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.htmlhelpers.js --- a/web/data/cubicweb.htmlhelpers.js Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.htmlhelpers.js Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.js --- a/web/data/cubicweb.js Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.js Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.old.css --- a/web/data/cubicweb.old.css Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.old.css Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.pictograms.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/cubicweb.pictograms.css Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.rhythm.js --- a/web/data/cubicweb.rhythm.js Tue Jan 21 14:56:06 2014 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -$(document).ready(function() { - $('a.rhythm').click(function (event){ - $('div#pageContent').toggleClass('rhythm_bg'); - $('div#page').toggleClass('rhythm_bg'); - event.preventDefault(); - }); -}); diff -r 1910d86afcbc -r 6880674c1a26 web/data/cubicweb.widgets.js --- a/web/data/cubicweb.widgets.js Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/cubicweb.widgets.js Tue Jan 21 15:11:16 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 1910d86afcbc -r 6880674c1a26 web/data/entypo.eot Binary file web/data/entypo.eot has changed diff -r 1910d86afcbc -r 6880674c1a26 web/data/entypo.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/entypo.svg Tue Jan 21 15:11:16 2014 +0100 @@ -0,0 +1,295 @@ + + + +Copyright (C) 2012 by Daniel Bruce + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff -r 1910d86afcbc -r 6880674c1a26 web/data/entypo.ttf Binary file web/data/entypo.ttf has changed diff -r 1910d86afcbc -r 6880674c1a26 web/data/entypo.woff Binary file web/data/entypo.woff has changed diff -r 1910d86afcbc -r 6880674c1a26 web/data/fullcalendar.css --- a/web/data/fullcalendar.css Tue Jan 21 14:56:06 2014 +0100 +++ b/web/data/fullcalendar.css Tue Jan 21 15:11:16 2014 +0100 @@ -1,24 +1,10 @@ -/* - * FullCalendar v1.4.8 Stylesheet - * - * Feel free to edit this file to customize the look of FullCalendar. - * When upgrading to newer versions, please upgrade this file as well, - * porting over any customizations afterwards. - * - * Date: Sat Oct 16 17:10:03 2010 -0700 - * +/*! + * FullCalendar v1.6.4 Stylesheet + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw */ - - -/* TODO: make font sizes look the same in all doctypes */ -.fc, -.fc .fc-header, -.fc .fc-content { - font-size: 1em; - } - .fc { direction: ltr; text-align: left; @@ -29,7 +15,13 @@ border-spacing: 0; } -.fc td, .fc th { +html .fc, +.fc table { + font-size: 1em; + } + +.fc td, +.fc th { padding: 0; vertical-align: top; } @@ -38,242 +30,262 @@ /* Header ------------------------------------------------------------------------*/ - -table.fc-header { - width: 100%; + +.fc-header td { + white-space: nowrap; } - + .fc-header-left { width: 25%; - } - -.fc-header-left table { - float: left; + text-align: left; } .fc-header-center { - width: 50%; text-align: center; } -.fc-header-center table { - margin: 0 auto; - } - .fc-header-right { width: 25%; - } - -.fc-header-right table { - float: right; + text-align: right; } .fc-header-title { + display: inline-block; + vertical-align: top; + } + +.fc-header-title h2 { margin-top: 0; white-space: nowrap; } -.fc-header-space { +.fc .fc-header-space { padding-left: 10px; } -/* right-to-left */ +.fc-header .fc-button { + margin-bottom: 1em; + vertical-align: top; + } + +/* buttons edges butting together */ -.fc-rtl .fc-header-title { - direction: rtl; +.fc-header .fc-button { + margin-right: -1px; + } + +.fc-header .fc-corner-right, /* non-theme */ +.fc-header .ui-corner-right { /* theme */ + margin-right: 0; /* back to normal */ + } + +/* button layering (for border precedence) */ + +.fc-header .fc-state-hover, +.fc-header .ui-state-hover { + z-index: 2; + } + +.fc-header .fc-state-down { + z-index: 3; } +.fc-header .fc-state-active, +.fc-header .ui-state-active { + z-index: 4; + } + + + +/* Content +------------------------------------------------------------------------*/ + +.fc-content { + clear: both; + zoom: 1; /* for IE7, gives accurate coordinates for [un]freezeContentHeight */ + } + +.fc-view { + width: 100%; + overflow: hidden; + } + + + +/* Cell Styles +------------------------------------------------------------------------*/ + +.fc-widget-header, /* , usually */ +.fc-widget-content { /* , usually */ + border: 1px solid #ddd; + } + +.fc-state-highlight { /* today cell */ /* TODO: add .fc-today to */ + background: #fcf8e3; + } + +.fc-cell-overlay { /* semi-transparent rectangle while dragging */ + background: #bce8f1; + opacity: .3; + filter: alpha(opacity=30); /* for IE */ + } + /* Buttons ------------------------------------------------------------------------*/ -.fc-header .fc-state-default, -.fc-header .ui-state-default { - margin-bottom: 1em; +.fc-button { + position: relative; + display: inline-block; + padding: 0 .6em; + overflow: hidden; + height: 1.9em; + line-height: 1.9em; + white-space: nowrap; cursor: pointer; } -.fc-header .fc-state-default { - border-width: 1px 0; - padding: 0 1px; - } - -.fc-header .fc-state-default, -.fc-header .fc-state-default a { - border-style: solid; +.fc-state-default { /* non-theme */ + border: 1px solid; } - -.fc-header .fc-state-default a { - display: block; - border-width: 0 1px; - margin: 0 -1px; - width: 100%; - text-decoration: none; - } - -.fc-header .fc-state-default span { - display: block; - border-style: solid; - border-width: 1px 0 1px 1px; - padding: 3px 5px; - } - -.fc-header .ui-state-default { - padding: 4px 6px; - } - -.fc-header .fc-state-default span, -.fc-header .ui-state-default span { - white-space: nowrap; + +.fc-state-default.fc-corner-left { /* non-theme */ + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; } - -/* for adjacent buttons */ - -.fc-header .fc-no-right { - padding-right: 0; - } - -.fc-header .fc-no-right a { - margin-right: 0; - border-right: 0; - } - -.fc-header .ui-no-right { - border-right: 0; + +.fc-state-default.fc-corner-right { /* non-theme */ + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; } - -/* for fake rounded corners */ - -.fc-header .fc-corner-left { - margin-left: 1px; - padding-left: 0; - } - -.fc-header .fc-corner-right { - margin-right: 1px; - padding-right: 0; - } - -/* DEFAULT button COLORS */ - -.fc-header .fc-state-default, -.fc-header .fc-state-default a { - border-color: #777; /* outer border */ - color: #333; + +/* + Our default prev/next buttons use HTML entities like ‹ › « » + and we'll try to make them look good cross-browser. +*/ + +.fc-text-arrow { + margin: 0 .1em; + font-size: 2em; + font-family: "Courier New", Courier, monospace; + vertical-align: baseline; /* for IE7 */ } -.fc-header .fc-state-default span { - border-color: #fff #fff #d1d1d1; /* inner border */ - background: #e8e8e8; - } - -/* PRESSED button COLORS (down and active) */ - -.fc-header .fc-state-active a { - color: #fff; +.fc-button-prev .fc-text-arrow, +.fc-button-next .fc-text-arrow { /* for ‹ › */ + font-weight: bold; } -.fc-header .fc-state-down span, -.fc-header .fc-state-active span { - background: #888; - border-color: #808080 #808080 #909090; /* inner border */ +/* icon (for jquery ui) */ + +.fc-button .fc-icon-wrap { + position: relative; + float: left; + top: 50%; } -/* DISABLED button COLORS */ - -.fc-header .fc-state-disabled a { - color: #999; - } - -.fc-header .fc-state-disabled, -.fc-header .fc-state-disabled a { - border-color: #ccc; /* outer border */ - } - -.fc-header .fc-state-disabled span { - border-color: #fff #fff #f0f0f0; /* inner border */ - background: #f0f0f0; +.fc-button .ui-icon { + position: relative; + float: left; + margin-top: -50%; + *margin-top: 0; + *top: -50%; } - - -/* Content Area & Global Cell Styles -------------------------------------------------------------------------*/ - -.fc-widget-content { - border: 1px solid #ccc; /* outer border color */ +/* + button states + borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/) +*/ + +.fc-state-default { + background-color: #f5f5f5; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + color: #333; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + } + +.fc-state-hover, +.fc-state-down, +.fc-state-active, +.fc-state-disabled { + color: #333333; + background-color: #e6e6e6; } - -.fc-content { - clear: both; + +.fc-state-hover { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; } - -.fc-content .fc-state-default { - border-style: solid; - border-color: #ccc; /* inner border color */ + +.fc-state-down, +.fc-state-active { + background-color: #cccccc; + background-image: none; + outline: 0; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); } + +.fc-state-disabled { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + box-shadow: none; + } + -.fc-content .fc-state-highlight { /* today */ - background: #ffc; - } - -.fc-content .fc-not-today { /* override jq-ui highlight (TODO: ui-widget-content) */ - background: none; - } - -.fc-cell-overlay { /* semi-transparent rectangle while dragging */ - background: #9cf; - opacity: .2; - filter: alpha(opacity=20); /* for IE */ - } - -.fc-view { /* prevents dragging outside of widget */ - width: 100%; - overflow: hidden; - } - - - - /* Global Event Styles ------------------------------------------------------------------------*/ -.fc-event, -.fc-agenda .fc-event-time, -.fc-event a { - border-style: solid; - border-color: #36c; /* default BORDER color (probably the same as background-color) */ - background-color: #36c; /* default BACKGROUND color */ - color: #fff; /* default TEXT color */ +.fc-event-container > * { + z-index: 8; + } + +.fc-event-container > .ui-draggable-dragging, +.fc-event-container > .ui-resizable-resizing { + z-index: 9; + } + +.fc-event { + border: 1px solid #3a87ad; /* default BORDER color */ + background-color: #3a87ad; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ + font-size: .85em; + cursor: default; + } + +a.fc-event { + text-decoration: none; } - /* Use the 'className' CalEvent property and the following - * example CSS to change event color on a per-event basis: - * - * .myclass, - * .fc-agenda .myclass .fc-event-time, - * .myclass a { - * background-color: black; - * border-color: black; - * color: red; - * } - */ - -.fc-event { - text-align: left; - } - -.fc-event a { - overflow: hidden; - font-size: .85em; - text-decoration: none; +a.fc-event, +.fc-event-draggable { cursor: pointer; } -.fc-event-editable { - cursor: pointer; +.fc-rtl .fc-event { + text-align: right; + } + +.fc-event-inner { + width: 100%; + height: 100%; + overflow: hidden; } .fc-event-time, @@ -281,29 +293,13 @@ padding: 0 1px; } -/* for fake rounded corners */ - -.fc-event a { - display: block; - position: relative; - width: 100%; - height: 100%; - } - -/* right-to-left */ - -.fc-rtl .fc-event a { - text-align: right; - } - -/* resizable */ - .fc .ui-resizable-handle { display: block; position: absolute; z-index: 99999; - border: 0 !important; /* important overrides pre jquery ui 1.7 styles */ - background: url(data:image/gif;base64,AAAA) !important; /* hover fix for IE */ + overflow: hidden; /* hacky spaces (IE6/7) */ + font-size: 300%; /* */ + line-height: 50%; /* */ } @@ -315,29 +311,19 @@ border-width: 1px 0; margin-bottom: 1px; } - -.fc-event-hori a { - border-width: 0; - } - -/* for fake rounded corners */ - -.fc-content .fc-corner-left { - margin-left: 1px; + +.fc-ltr .fc-event-hori.fc-event-start, +.fc-rtl .fc-event-hori.fc-event-end { + border-left-width: 1px; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; } - -.fc-content .fc-corner-left a { - margin-left: -1px; - border-left-width: 1px; - } - -.fc-content .fc-corner-right { - margin-right: 1px; - } - -.fc-content .fc-corner-right a { - margin-right: -1px; + +.fc-ltr .fc-event-hori.fc-event-end, +.fc-rtl .fc-event-hori.fc-event-start { border-right-width: 1px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; } /* resizable */ @@ -364,26 +350,49 @@ +/* Reusable Separate-border Table +------------------------------------------------------------*/ + +table.fc-border-separate { + border-collapse: separate; + } + +.fc-border-separate th, +.fc-border-separate td { + border-width: 1px 0 0 1px; + } + +.fc-border-separate th.fc-last, +.fc-border-separate td.fc-last { + border-right-width: 1px; + } + +.fc-border-separate tr.fc-last th, +.fc-border-separate tr.fc-last td { + border-bottom-width: 1px; + } + +.fc-border-separate tbody tr.fc-first td, +.fc-border-separate tbody tr.fc-first th { + border-top-width: 0; + } + + /* Month View, Basic Week View, Basic Day View ------------------------------------------------------------------------*/ -.fc-grid table { - width: 100%; - } - -.fc .fc-grid th { - border-width: 0 0 0 1px; +.fc-grid th { text-align: center; } - -.fc .fc-grid td { - border-width: 1px 0 0 1px; + +.fc .fc-week-number { + width: 22px; + text-align: center; } - -.fc-grid th.fc-leftmost, -.fc-grid td.fc-leftmost { - border-left: 0; + +.fc .fc-week-number div { + padding: 0 2px; } .fc-grid .fc-day-number { @@ -401,7 +410,7 @@ .fc-grid .fc-day-content { clear: both; - padding: 2px 2px 0; /* distance between events and day edges */ + padding: 2px 2px 1px; /* distance between events and day edges */ } /* event styles */ @@ -411,10 +420,6 @@ } /* right-to-left */ - -.fc-rtl .fc-grid { - direction: rtl; - } .fc-rtl .fc-grid .fc-day-number { float: left; @@ -424,85 +429,95 @@ float: right; } + + /* Agenda Week View, Agenda Day View ------------------------------------------------------------------------*/ -.fc .fc-agenda th, -.fc .fc-agenda td { - border-width: 1px 0 0 1px; - } - -.fc .fc-agenda .fc-leftmost { - border-left: 0; +.fc-agenda table { + border-collapse: separate; } -.fc-agenda tr.fc-first th, -.fc-agenda tr.fc-first td { - border-top: 0; - } - -.fc-agenda-head tr.fc-last th { - border-bottom-width: 1px; - } - -.fc .fc-agenda-head td, -.fc .fc-agenda-body td { - background: none; - } - -.fc-agenda-head th { +.fc-agenda-days th { text-align: center; } -/* the time axis running down the left side */ - -.fc-agenda .fc-axis { +.fc-agenda .fc-agenda-axis { width: 50px; padding: 0 4px; vertical-align: middle; + text-align: right; white-space: nowrap; - text-align: right; font-weight: normal; } + +.fc-agenda .fc-week-number { + font-weight: bold; + } + +.fc-agenda .fc-day-content { + padding: 2px 2px 1px; + } -/* all-day event cells at top */ +/* make axis border take precedence */ -.fc-agenda-head tr.fc-all-day th { - height: 35px; +.fc-agenda-days .fc-agenda-axis { + border-right-width: 1px; + } + +.fc-agenda-days .fc-col0 { + border-left-width: 0; } -.fc-agenda-head td { - padding-bottom: 10px; +/* all-day area */ + +.fc-agenda-allday th { + border-width: 0 1px; + } + +.fc-agenda-allday .fc-day-content { + min-height: 34px; /* TODO: doesnt work well in quirksmode */ + _height: 34px; } -.fc .fc-divider div { - font-size: 1px; /* for IE6/7 */ +/* divider (between all-day and slots) */ + +.fc-agenda-divider-inner { height: 2px; + overflow: hidden; + } + +.fc-widget-header .fc-agenda-divider-inner { + background: #eee; } -.fc .fc-divider .fc-state-default { - background: #eee; /* color for divider between all-day and time-slot events */ +/* slot rows */ + +.fc-agenda-slots th { + border-width: 1px 1px 0; + } + +.fc-agenda-slots td { + border-width: 1px 0 0; + background: none; + } + +.fc-agenda-slots td div { + height: 20px; + } + +.fc-agenda-slots tr.fc-slot0 th, +.fc-agenda-slots tr.fc-slot0 td { + border-top-width: 0; } -/* body styles */ - -.fc .fc-agenda-body td div { - height: 20px; /* slot height */ - } - -.fc .fc-agenda-body tr.fc-minor th, -.fc .fc-agenda-body tr.fc-minor td { +.fc-agenda-slots tr.fc-minor th, +.fc-agenda-slots tr.fc-minor td { border-top-style: dotted; } -.fc-agenda .fc-day-content { - padding: 2px 2px 0; /* distance between events and day edges */ - } - -/* vertical background columns */ - -.fc .fc-agenda-bg .ui-state-highlight { - background-image: none; /* tall column, don't want repeating background image */ +.fc-agenda-slots tr.fc-minor th.ui-widget-header { + *border-top-style: solid; /* doesn't work with background in IE6/7 */ } @@ -513,52 +528,30 @@ .fc-event-vert { border-width: 0 1px; } - -.fc-event-vert a { - border-width: 0; + +.fc-event-vert.fc-event-start { + border-top-width: 1px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; } - -/* for fake rounded corners */ - -.fc-content .fc-corner-top { - margin-top: 1px; + +.fc-event-vert.fc-event-end { + border-bottom-width: 1px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; } -.fc-content .fc-corner-top a { - margin-top: -1px; - border-top-width: 1px; - } - -.fc-content .fc-corner-bottom { - margin-bottom: 1px; +.fc-event-vert .fc-event-time { + white-space: nowrap; + font-size: 10px; } - -.fc-content .fc-corner-bottom a { - margin-bottom: -1px; - border-bottom-width: 1px; - } - -/* event content */ - -.fc-event-vert span { - display: block; + +.fc-event-vert .fc-event-inner { position: relative; z-index: 2; } -.fc-event-vert span.fc-event-time { - white-space: nowrap; - _white-space: normal; - overflow: hidden; - border: 0; - font-size: 10px; - } - -.fc-event-vert span.fc-event-title { - line-height: 13px; - } - -.fc-event-vert span.fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ +.fc-event-vert .fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ position: absolute; z-index: 1; top: 0; @@ -566,8 +559,13 @@ width: 100%; height: 100%; background: #fff; - opacity: .3; - filter: alpha(opacity=30); /* for IE */ + opacity: .25; + filter: alpha(opacity=25); + } + +.fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */ +.fc-select-helper .fc-event-bg { + display: none\9; /* for IE6/7/8. nested opacity filters while dragging don't work */ } /* resizable */ @@ -576,6 +574,7 @@ bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */ width: 100% !important; height: 8px !important; + overflow: hidden !important; line-height: 8px !important; font-size: 11px !important; font-family: monospace; @@ -583,4 +582,8 @@ cursor: s-resize; } +.fc-agenda .ui-resizable-resizing { /* TODO: better selector */ + _overflow: hidden; + } + diff -r 1910d86afcbc -r 6880674c1a26 web/data/fullcalendar.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/fullcalendar.js Tue Jan 21 15:11:16 2014 +0100 @@ -0,0 +1,6110 @@ +/*! + * FullCalendar v1.6.4 + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + +/* + * Use fullcalendar.css for basic styling. + * For event drag & drop, requires jQuery UI draggable. + * For event resizing, requires jQuery UI resizable. + */ + +(function($, undefined) { + + +;; + +var defaults = { + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + weekNumbers: false, + weekNumberCalculation: 'iso', + weekNumberTitle: 'W', + + // editing + //editable: false, + //disableDragging: false, + //disableResizing: false, + + allDayDefault: true, + ignoreTimezone: true, + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + + // time formats + titleFormat: { + month: 'MMMM yyyy', + week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", + day: 'dddd, MMM d, yyyy' + }, + columnFormat: { + month: 'ddd', + week: 'ddd M/d', + day: 'dddd M/d' + }, + timeFormat: { // for event elements + '': 'h(:mm)t' // default + }, + + // locale + isRTL: false, + firstDay: 0, + monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'], + monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], + dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], + dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], + buttonText: { + prev: "", + next: "", + prevYear: "«", + nextYear: "»", + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + // jquery-ui theming + theme: false, + buttonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e' + }, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*', + + handleWindowResize: true + +}; + +// right-to-left defaults +var rtlDefaults = { + header: { + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonText: { + prev: "", + next: "", + prevYear: "»", + nextYear: "«" + }, + buttonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w' + } +}; + + + +;; + +var fc = $.fullCalendar = { version: "1.6.4" }; +var fcViews = fc.views = {}; + + +$.fn.fullCalendar = function(options) { + + + // method calling + if (typeof options == 'string') { + var args = Array.prototype.slice.call(arguments, 1); + var res; + this.each(function() { + var calendar = $.data(this, 'fullCalendar'); + if (calendar && $.isFunction(calendar[options])) { + var r = calendar[options].apply(calendar, args); + if (res === undefined) { + res = r; + } + if (options == 'destroy') { + $.removeData(this, 'fullCalendar'); + } + } + }); + if (res !== undefined) { + return res; + } + return this; + } + + options = options || {}; + + // would like to have this logic in EventManager, but needs to happen before options are recursively extended + var eventSources = options.eventSources || []; + delete options.eventSources; + if (options.events) { + eventSources.push(options.events); + delete options.events; + } + + + options = $.extend(true, {}, + defaults, + (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {}, + options + ); + + + this.each(function(i, _element) { + var element = $(_element); + var calendar = new Calendar(element, options, eventSources); + element.data('fullCalendar', calendar); // TODO: look into memory leak implications + calendar.render(); + }); + + + return this; + +}; + + +// function for adding/overriding defaults +function setDefaults(d) { + $.extend(true, defaults, d); +} + + + +;; + + +function Calendar(element, options, eventSources) { + var t = this; + + + // exports + t.options = options; + t.render = render; + t.destroy = destroy; + t.refetchEvents = refetchEvents; + t.reportEvents = reportEvents; + t.reportEventChange = reportEventChange; + t.rerenderEvents = rerenderEvents; + t.changeView = changeView; + t.select = select; + t.unselect = unselect; + t.prev = prev; + t.next = next; + t.prevYear = prevYear; + t.nextYear = nextYear; + t.today = today; + t.gotoDate = gotoDate; + t.incrementDate = incrementDate; + t.formatDate = function(format, date) { return formatDate(format, date, options) }; + t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) }; + t.getDate = getDate; + t.getView = getView; + t.option = option; + t.trigger = trigger; + + + // imports + EventManager.call(t, options, eventSources); + var isFetchNeeded = t.isFetchNeeded; + var fetchEvents = t.fetchEvents; + + + // locals + var _element = element[0]; + var header; + var headerElement; + var content; + var tm; // for making theme classes + var currentView; + var elementOuterWidth; + var suggestedViewHeight; + var resizeUID = 0; + var ignoreWindowResize = 0; + var date = new Date(); + var events = []; + var _dragElement; + + + + /* Main Rendering + -----------------------------------------------------------------------------*/ + + + setYMD(date, options.year, options.month, options.date); + + + function render(inc) { + if (!content) { + initialRender(); + } + else if (elementVisible()) { + // mainly for the public API + calcSize(); + _renderView(inc); + } + } + + + function initialRender() { + tm = options.theme ? 'ui' : 'fc'; + element.addClass('fc'); + if (options.isRTL) { + element.addClass('fc-rtl'); + } + else { + element.addClass('fc-ltr'); + } + if (options.theme) { + element.addClass('ui-widget'); + } + + content = $("
    ") + .prependTo(element); + + header = new Header(t, options); + headerElement = header.render(); + if (headerElement) { + element.prepend(headerElement); + } + + changeView(options.defaultView); + + if (options.handleWindowResize) { + $(window).resize(windowResize); + } + + // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize + if (!bodyVisible()) { + lateRender(); + } + } + + + // called when we know the calendar couldn't be rendered when it was initialized, + // but we think it's ready now + function lateRender() { + setTimeout(function() { // IE7 needs this so dimensions are calculated correctly + if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once + renderView(); + } + },0); + } + + + function destroy() { + + if (currentView) { + trigger('viewDestroy', currentView, currentView, currentView.element); + currentView.triggerEventDestroy(); + } + + $(window).unbind('resize', windowResize); + + header.destroy(); + content.remove(); + element.removeClass('fc fc-rtl ui-widget'); + } + + + function elementVisible() { + return element.is(':visible'); + } + + + function bodyVisible() { + return $('body').is(':visible'); + } + + + + /* View Rendering + -----------------------------------------------------------------------------*/ + + + function changeView(newViewName) { + if (!currentView || newViewName != currentView.name) { + _changeView(newViewName); + } + } + + + function _changeView(newViewName) { + ignoreWindowResize++; + + if (currentView) { + trigger('viewDestroy', currentView, currentView, currentView.element); + unselect(); + currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event + freezeContentHeight(); + currentView.element.remove(); + header.deactivateButton(currentView.name); + } + + header.activateButton(newViewName); + + currentView = new fcViews[newViewName]( + $("
    ") + .appendTo(content), + t // the calendar object + ); + + renderView(); + unfreezeContentHeight(); + + ignoreWindowResize--; + } + + + function renderView(inc) { + if ( + !currentView.start || // never rendered before + inc || date < currentView.start || date >= currentView.end // or new date range + ) { + if (elementVisible()) { + _renderView(inc); + } + } + } + + + function _renderView(inc) { // assumes elementVisible + ignoreWindowResize++; + + if (currentView.start) { // already been rendered? + trigger('viewDestroy', currentView, currentView, currentView.element); + unselect(); + clearEvents(); + } + + freezeContentHeight(); + currentView.render(date, inc || 0); // the view's render method ONLY renders the skeleton, nothing else + setSize(); + unfreezeContentHeight(); + (currentView.afterRender || noop)(); + + updateTitle(); + updateTodayButton(); + + trigger('viewRender', currentView, currentView, currentView.element); + currentView.trigger('viewDisplay', _element); // deprecated + + ignoreWindowResize--; + + getAndRenderEvents(); + } + + + + /* Resizing + -----------------------------------------------------------------------------*/ + + + function updateSize() { + if (elementVisible()) { + unselect(); + clearEvents(); + calcSize(); + setSize(); + renderEvents(); + } + } + + + function calcSize() { // assumes elementVisible + if (options.contentHeight) { + suggestedViewHeight = options.contentHeight; + } + else if (options.height) { + suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content); + } + else { + suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); + } + } + + + function setSize() { // assumes elementVisible + + if (suggestedViewHeight === undefined) { + calcSize(); // for first time + // NOTE: we don't want to recalculate on every renderView because + // it could result in oscillating heights due to scrollbars. + } + + ignoreWindowResize++; + currentView.setHeight(suggestedViewHeight); + currentView.setWidth(content.width()); + ignoreWindowResize--; + + elementOuterWidth = element.outerWidth(); + } + + + function windowResize() { + if (!ignoreWindowResize) { + if (currentView.start) { // view has already been rendered + var uid = ++resizeUID; + setTimeout(function() { // add a delay + if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { + if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { + ignoreWindowResize++; // in case the windowResize callback changes the height + updateSize(); + currentView.trigger('windowResize', _element); + ignoreWindowResize--; + } + } + }, 200); + }else{ + // calendar must have been initialized in a 0x0 iframe that has just been resized + lateRender(); + } + } + } + + + + /* Event Fetching/Rendering + -----------------------------------------------------------------------------*/ + // TODO: going forward, most of this stuff should be directly handled by the view + + + function refetchEvents() { // can be called as an API method + clearEvents(); + fetchAndRenderEvents(); + } + + + function rerenderEvents(modifiedEventID) { // can be called as an API method + clearEvents(); + renderEvents(modifiedEventID); + } + + + function renderEvents(modifiedEventID) { // TODO: remove modifiedEventID hack + if (elementVisible()) { + currentView.setEventData(events); // for View.js, TODO: unify with renderEvents + currentView.renderEvents(events, modifiedEventID); // actually render the DOM elements + currentView.trigger('eventAfterAllRender'); + } + } + + + function clearEvents() { + currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event + currentView.clearEvents(); // actually remove the DOM elements + currentView.clearEventData(); // for View.js, TODO: unify with clearEvents + } + + + function getAndRenderEvents() { + if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) { + fetchAndRenderEvents(); + } + else { + renderEvents(); + } + } + + + function fetchAndRenderEvents() { + fetchEvents(currentView.visStart, currentView.visEnd); + // ... will call reportEvents + // ... which will call renderEvents + } + + + // called when event data arrives + function reportEvents(_events) { + events = _events; + renderEvents(); + } + + + // called when a single event's data has been changed + function reportEventChange(eventID) { + rerenderEvents(eventID); + } + + + + /* Header Updating + -----------------------------------------------------------------------------*/ + + + function updateTitle() { + header.updateTitle(currentView.title); + } + + + function updateTodayButton() { + var today = new Date(); + if (today >= currentView.start && today < currentView.end) { + header.disableButton('today'); + } + else { + header.enableButton('today'); + } + } + + + + /* Selection + -----------------------------------------------------------------------------*/ + + + function select(start, end, allDay) { + currentView.select(start, end, allDay===undefined ? true : allDay); + } + + + function unselect() { // safe to be called before renderView + if (currentView) { + currentView.unselect(); + } + } + + + + /* Date + -----------------------------------------------------------------------------*/ + + + function prev() { + renderView(-1); + } + + + function next() { + renderView(1); + } + + + function prevYear() { + addYears(date, -1); + renderView(); + } + + + function nextYear() { + addYears(date, 1); + renderView(); + } + + + function today() { + date = new Date(); + renderView(); + } + + + function gotoDate(year, month, dateOfMonth) { + if (year instanceof Date) { + date = cloneDate(year); // provided 1 argument, a Date + }else{ + setYMD(date, year, month, dateOfMonth); + } + renderView(); + } + + + function incrementDate(years, months, days) { + if (years !== undefined) { + addYears(date, years); + } + if (months !== undefined) { + addMonths(date, months); + } + if (days !== undefined) { + addDays(date, days); + } + renderView(); + } + + + function getDate() { + return cloneDate(date); + } + + + + /* Height "Freezing" + -----------------------------------------------------------------------------*/ + + + function freezeContentHeight() { + content.css({ + width: '100%', + height: content.height(), + overflow: 'hidden' + }); + } + + + function unfreezeContentHeight() { + content.css({ + width: '', + height: '', + overflow: '' + }); + } + + + + /* Misc + -----------------------------------------------------------------------------*/ + + + function getView() { + return currentView; + } + + + function option(name, value) { + if (value === undefined) { + return options[name]; + } + if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { + options[name] = value; + updateSize(); + } + } + + + function trigger(name, thisObj) { + if (options[name]) { + return options[name].apply( + thisObj || _element, + Array.prototype.slice.call(arguments, 2) + ); + } + } + + + + /* External Dragging + ------------------------------------------------------------------------*/ + + if (options.droppable) { + $(document) + .bind('dragstart', function(ev, ui) { + var _e = ev.target; + var e = $(_e); + if (!e.parents('.fc').length) { // not already inside a calendar + var accept = options.dropAccept; + if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) { + _dragElement = _e; + currentView.dragStart(_dragElement, ev, ui); + } + } + }) + .bind('dragstop', function(ev, ui) { + if (_dragElement) { + currentView.dragStop(_dragElement, ev, ui); + _dragElement = null; + } + }); + } + + +} + +;; + +function Header(calendar, options) { + var t = this; + + + // exports + t.render = render; + t.destroy = destroy; + t.updateTitle = updateTitle; + t.activateButton = activateButton; + t.deactivateButton = deactivateButton; + t.disableButton = disableButton; + t.enableButton = enableButton; + + + // locals + var element = $([]); + var tm; + + + + function render() { + tm = options.theme ? 'ui' : 'fc'; + var sections = options.header; + if (sections) { + element = $("") + .append( + $("") + .append(renderSection('left')) + .append(renderSection('center')) + .append(renderSection('right')) + ); + return element; + } + } + + + function destroy() { + element.remove(); + } + + + function renderSection(position) { + var e = $(""; + + if (showWeekNumbers) { + html += + ""; + } + + for (col=0; col" + + htmlEscape(formatDate(date, colFormat)) + + ""; + } + + html += ""; + + return html; + } + + + function buildBodyHTML() { + var contentClass = tm + "-widget-content"; + var html = ''; + var row; + var col; + var date; + + html += ""; + + for (row=0; row" + + "
    " + + htmlEscape(formatDate(date, weekNumberFormat)) + + "
    " + + ""; + } + + for (col=0; col" + + "
    "; + + if (showNumbers) { + html += "
    " + date.getDate() + "
    "; + } + + html += + "
    " + + "
     
    " + + "
    " + + "
    " + + ""; + + return html; + } + + + + /* Dimensions + -----------------------------------------------------------*/ + + + function setHeight(height) { + viewHeight = height; + + var bodyHeight = viewHeight - head.height(); + var rowHeight; + var rowHeightLast; + var cell; + + if (opt('weekMode') == 'variable') { + rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6)); + }else{ + rowHeight = Math.floor(bodyHeight / rowCnt); + rowHeightLast = bodyHeight - rowHeight * (rowCnt-1); + } + + bodyFirstCells.each(function(i, _cell) { + if (i < rowCnt) { + cell = $(_cell); + cell.find('> div').css( + 'min-height', + (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell) + ); + } + }); + + } + + + function setWidth(width) { + viewWidth = width; + colPositions.clear(); + colContentPositions.clear(); + + weekNumberWidth = 0; + if (showWeekNumbers) { + weekNumberWidth = head.find('th.fc-week-number').outerWidth(); + } + + colWidth = Math.floor((viewWidth - weekNumberWidth) / colCnt); + setOuterWidth(headCells.slice(0, -1), colWidth); + } + + + + /* Day clicking and binding + -----------------------------------------------------------*/ + + + function dayBind(days) { + days.click(dayClick) + .mousedown(daySelectionMousedown); + } + + + function dayClick(ev) { + if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick + var date = parseISO8601($(this).data('date')); + trigger('dayClick', this, date, true, ev); + } + } + + + + /* Semi-transparent Overlay Helpers + ------------------------------------------------------*/ + // TODO: should be consolidated with AgendaView's methods + + + function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive + + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + + var segments = rangeToSegments(overlayStart, overlayEnd); + + for (var i=0; i") + .appendTo(element); + + if (opt('allDaySlot')) { + + daySegmentContainer = + $("
    ") + .appendTo(slotLayer); + + s = + "
    "); + var buttonStr = options.header[position]; + if (buttonStr) { + $.each(buttonStr.split(' '), function(i) { + if (i > 0) { + e.append(""); + } + var prevButton; + $.each(this.split(','), function(j, buttonName) { + if (buttonName == 'title') { + e.append("

     

    "); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + prevButton = null; + }else{ + var buttonClick; + if (calendar[buttonName]) { + buttonClick = calendar[buttonName]; // calendar method + } + else if (fcViews[buttonName]) { + buttonClick = function() { + button.removeClass(tm + '-state-hover'); // forget why + calendar.changeView(buttonName); + }; + } + if (buttonClick) { + var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here? + var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here? + var button = $( + "" + + (icon ? + "" + + "" + + "" : + text + ) + + "" + ) + .click(function() { + if (!button.hasClass(tm + '-state-disabled')) { + buttonClick(); + } + }) + .mousedown(function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); + } + ) + .appendTo(e); + disableTextSelection(button); + if (!prevButton) { + button.addClass(tm + '-corner-left'); + } + prevButton = button; + } + } + }); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + }); + } + return e; + } + + + function updateTitle(html) { + element.find('h2') + .html(html); + } + + + function activateButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .addClass(tm + '-state-active'); + } + + + function deactivateButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .removeClass(tm + '-state-active'); + } + + + function disableButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .addClass(tm + '-state-disabled'); + } + + + function enableButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .removeClass(tm + '-state-disabled'); + } + + +} + +;; + +fc.sourceNormalizers = []; +fc.sourceFetchers = []; + +var ajaxDefaults = { + dataType: 'json', + cache: false +}; + +var eventGUID = 1; + + +function EventManager(options, _sources) { + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.addEventSource = addEventSource; + t.removeEventSource = removeEventSource; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.normalizeEvent = normalizeEvent; + + + // imports + var trigger = t.trigger; + var getView = t.getView; + var reportEvents = t.reportEvents; + + + // locals + var stickySource = { events: [] }; + var sources = [ stickySource ]; + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var loadingLevel = 0; + var cache = []; + + + for (var i=0; i<_sources.length; i++) { + _addEventSource(_sources[i]); + } + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + function isFetchNeeded(start, end) { + return !rangeStart || start < rangeStart || end > rangeEnd; + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + cache = []; + var fetchID = ++currentFetchID; + var len = sources.length; + pendingSourceCnt = len; + for (var i=0; i)), return null instead + return null; +} + + +function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false + // derived from http://delete.me.uk/2005/03/iso8601.html + // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html + var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/); + if (!m) { + return null; + } + var date = new Date(m[1], 0, 1); + if (ignoreTimezone || !m[13]) { + var check = new Date(m[1], 0, 1, 9, 0); + if (m[3]) { + date.setMonth(m[3] - 1); + check.setMonth(m[3] - 1); + } + if (m[5]) { + date.setDate(m[5]); + check.setDate(m[5]); + } + fixDate(date, check); + if (m[7]) { + date.setHours(m[7]); + } + if (m[8]) { + date.setMinutes(m[8]); + } + if (m[10]) { + date.setSeconds(m[10]); + } + if (m[12]) { + date.setMilliseconds(Number("0." + m[12]) * 1000); + } + fixDate(date, check); + }else{ + date.setUTCFullYear( + m[1], + m[3] ? m[3] - 1 : 0, + m[5] || 1 + ); + date.setUTCHours( + m[7] || 0, + m[8] || 0, + m[10] || 0, + m[12] ? Number("0." + m[12]) * 1000 : 0 + ); + if (m[14]) { + var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0); + offset *= m[15] == '-' ? 1 : -1; + date = new Date(+date + (offset * 60 * 1000)); + } + } + return date; +} + + +function parseTime(s) { // returns minutes since start of day + if (typeof s == 'number') { // an hour + return s * 60; + } + if (typeof s == 'object') { // a Date object + return s.getHours() * 60 + s.getMinutes(); + } + var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/); + if (m) { + var h = parseInt(m[1], 10); + if (m[3]) { + h %= 12; + if (m[3].toLowerCase().charAt(0) == 'p') { + h += 12; + } + } + return h * 60 + (m[2] ? parseInt(m[2], 10) : 0); + } +} + + + +/* Date Formatting +-----------------------------------------------------------------------------*/ +// TODO: use same function formatDate(date, [date2], format, [options]) + + +function formatDate(date, format, options) { + return formatDates(date, null, format, options); +} + + +function formatDates(date1, date2, format, options) { + options = options || defaults; + var date = date1, + otherDate = date2, + i, len = format.length, c, + i2, formatter, + res = ''; + for (i=0; ii; i2--) { + if (formatter = dateFormatters[format.substring(i, i2)]) { + if (date) { + res += formatter(date, options); + } + i = i2 - 1; + break; + } + } + if (i2 == i) { + if (date) { + res += c; + } + } + } + } + return res; +}; + + +var dateFormatters = { + s : function(d) { return d.getSeconds() }, + ss : function(d) { return zeroPad(d.getSeconds()) }, + m : function(d) { return d.getMinutes() }, + mm : function(d) { return zeroPad(d.getMinutes()) }, + h : function(d) { return d.getHours() % 12 || 12 }, + hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, + H : function(d) { return d.getHours() }, + HH : function(d) { return zeroPad(d.getHours()) }, + d : function(d) { return d.getDate() }, + dd : function(d) { return zeroPad(d.getDate()) }, + ddd : function(d,o) { return o.dayNamesShort[d.getDay()] }, + dddd: function(d,o) { return o.dayNames[d.getDay()] }, + M : function(d) { return d.getMonth() + 1 }, + MM : function(d) { return zeroPad(d.getMonth() + 1) }, + MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] }, + MMMM: function(d,o) { return o.monthNames[d.getMonth()] }, + yy : function(d) { return (d.getFullYear()+'').substring(2) }, + yyyy: function(d) { return d.getFullYear() }, + t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, + tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, + T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, + TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }, + u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") }, + S : function(d) { + var date = d.getDate(); + if (date > 10 && date < 20) { + return 'th'; + } + return ['st', 'nd', 'rd'][date%10-1] || 'th'; + }, + w : function(d, o) { // local + return o.weekNumberCalculation(d); + }, + W : function(d) { // ISO + return iso8601Week(d); + } +}; +fc.dateFormatters = dateFormatters; + + +/* thanks jQuery UI (https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js) + * + * Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. + * `date` - the date to get the week for + * `number` - the number of the week within the year that contains this date + */ +function iso8601Week(date) { + var time; + var checkDate = new Date(date.getTime()); + + // Find Thursday of this week starting on Monday + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); + + time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; +} + + +;; + +fc.applyAll = applyAll; + + +/* Event Date Math +-----------------------------------------------------------------------------*/ + + +function exclEndDay(event) { + if (event.end) { + return _exclEndDay(event.end, event.allDay); + }else{ + return addDays(cloneDate(event.start), 1); + } +} + + +function _exclEndDay(end, allDay) { + end = cloneDate(end); + return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end); + // why don't we check for seconds/ms too? +} + + + +/* Event Element Binding +-----------------------------------------------------------------------------*/ + + +function lazySegBind(container, segs, bindHandlers) { + container.unbind('mouseover').mouseover(function(ev) { + var parent=ev.target, e, + i, seg; + while (parent != this) { + e = parent; + parent = parent.parentNode; + } + if ((i = e._fci) !== undefined) { + e._fci = undefined; + seg = segs[i]; + bindHandlers(seg.event, seg.element, seg); + $(ev.target).trigger(ev); + } + ev.stopPropagation(); + }); +} + + + +/* Element Dimensions +-----------------------------------------------------------------------------*/ + + +function setOuterWidth(element, width, includeMargins) { + for (var i=0, e; i=0; i--) { + res = obj[parts[i].toLowerCase()]; + if (res !== undefined) { + return res; + } + } + return obj['']; +} + + +function htmlEscape(s) { + return s.replace(/&/g, '&') + .replace(//g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '
    '); +} + + +function disableTextSelection(element) { + element + .attr('unselectable', 'on') + .css('MozUserSelect', 'none') + .bind('selectstart.ui', function() { return false; }); +} + + +/* +function enableTextSelection(element) { + element + .attr('unselectable', 'off') + .css('MozUserSelect', '') + .unbind('selectstart.ui'); +} +*/ + + +function markFirstLast(e) { + e.children() + .removeClass('fc-first fc-last') + .filter(':first-child') + .addClass('fc-first') + .end() + .filter(':last-child') + .addClass('fc-last'); +} + + +function setDayID(cell, date) { + cell.each(function(i, _cell) { + _cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]); + // TODO: make a way that doesn't rely on order of classes + }); +} + + +function getSkinCss(event, opt) { + var source = event.source || {}; + var eventColor = event.color; + var sourceColor = source.color; + var optionColor = opt('eventColor'); + var backgroundColor = + event.backgroundColor || + eventColor || + source.backgroundColor || + sourceColor || + opt('eventBackgroundColor') || + optionColor; + var borderColor = + event.borderColor || + eventColor || + source.borderColor || + sourceColor || + opt('eventBorderColor') || + optionColor; + var textColor = + event.textColor || + source.textColor || + opt('eventTextColor'); + var statements = []; + if (backgroundColor) { + statements.push('background-color:' + backgroundColor); + } + if (borderColor) { + statements.push('border-color:' + borderColor); + } + if (textColor) { + statements.push('color:' + textColor); + } + return statements.join(';'); +} + + +function applyAll(functions, thisObj, args) { + if ($.isFunction(functions)) { + functions = [ functions ]; + } + if (functions) { + var i; + var ret; + for (i=0; i") + .appendTo(element); + } + + + function buildTable() { + var html = buildTableHTML(); + + if (table) { + table.remove(); + } + table = $(html).appendTo(element); + + head = table.find('thead'); + headCells = head.find('.fc-day-header'); + body = table.find('tbody'); + bodyRows = body.find('tr'); + bodyCells = body.find('.fc-day'); + bodyFirstCells = bodyRows.find('td:first-child'); + + firstRowCellInners = bodyRows.eq(0).find('.fc-day > div'); + firstRowCellContentInners = bodyRows.eq(0).find('.fc-day-content > div'); + + markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's + markFirstLast(bodyRows); // marks first+last td's + bodyRows.eq(0).addClass('fc-first'); + bodyRows.filter(':last').addClass('fc-last'); + + bodyCells.each(function(i, _cell) { + var date = cellToDate( + Math.floor(i / colCnt), + i % colCnt + ); + trigger('dayRender', t, date, $(_cell)); + }); + + dayBind(bodyCells); + } + + + + /* HTML Building + -----------------------------------------------------------*/ + + + function buildTableHTML() { + var html = + "" + + buildHeadHTML() + + buildBodyHTML() + + "
    "; + + return html; + } + + + function buildHeadHTML() { + var headerClass = tm + "-widget-header"; + var html = ''; + var col; + var date; + + html += "
    " + + htmlEscape(weekNumberTitle) + + "
    " + + "" + + "" + + "" + + "" + + "" + + "
    " + opt('allDayText') + "" + + "
    " + + "
     
    "; + allDayTable = $(s).appendTo(slotLayer); + allDayRow = allDayTable.find('tr'); + + dayBind(allDayRow.find('td')); + + slotLayer.append( + "
    " + + "
    " + + "
    " + ); + + }else{ + + daySegmentContainer = $([]); // in jQuery 1.4, we can just do $() + + } + + slotScroller = + $("
    ") + .appendTo(slotLayer); + + slotContainer = + $("
    ") + .appendTo(slotScroller); + + slotSegmentContainer = + $("
    ") + .appendTo(slotContainer); + + s = + "" + + ""; + d = zeroDate(); + maxd = addMinutes(cloneDate(d), maxMinute); + addMinutes(d, minMinute); + slotCnt = 0; + for (i=0; d < maxd; i++) { + minutes = d.getMinutes(); + s += + "" + + "" + + "" + + ""; + addMinutes(d, opt('slotMinutes')); + slotCnt++; + } + s += + "" + + "
    " + + ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') + + "" + + "
     
    " + + "
    "; + slotTable = $(s).appendTo(slotContainer); + + slotBind(slotTable.find('td')); + } + + + + /* Build Day Table + -----------------------------------------------------------------------*/ + + + function buildDayTable() { + var html = buildDayTableHTML(); + + if (dayTable) { + dayTable.remove(); + } + dayTable = $(html).appendTo(element); + + dayHead = dayTable.find('thead'); + dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter + dayBody = dayTable.find('tbody'); + dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter + dayBodyCellInners = dayBodyCells.find('> div'); + dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div'); + + dayBodyFirstCell = dayBodyCells.eq(0); + dayBodyFirstCellStretcher = dayBodyCellInners.eq(0); + + markFirstLast(dayHead.add(dayHead.find('tr'))); + markFirstLast(dayBody.add(dayBody.find('tr'))); + + // TODO: now that we rebuild the cells every time, we should call dayRender + } + + + function buildDayTableHTML() { + var html = + "" + + buildDayTableHeadHTML() + + buildDayTableBodyHTML() + + "
    "; + + return html; + } + + + function buildDayTableHeadHTML() { + var headerClass = tm + "-widget-header"; + var date; + var html = ''; + var weekText; + var col; + + html += + "" + + ""; + + if (showWeekNumbers) { + date = cellToDate(0, 0); + weekText = formatDate(date, weekNumberFormat); + if (rtl) { + weekText += weekNumberTitle; + } + else { + weekText = weekNumberTitle + weekText; + } + html += + "" + + htmlEscape(weekText) + + ""; + } + else { + html += " "; + } + + for (col=0; col" + + htmlEscape(formatDate(date, colFormat)) + + ""; + } + + html += + " " + + "" + + ""; + + return html; + } + + + function buildDayTableBodyHTML() { + var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called + var contentClass = tm + "-widget-content"; + var date; + var today = clearTime(new Date()); + var col; + var cellsHTML; + var cellHTML; + var classNames; + var html = ''; + + html += + "" + + "" + + " "; + + cellsHTML = ''; + + for (col=0; col" + + "
    " + + "
    " + + "
     
    " + + "
    " + + "
    " + + ""; + + cellsHTML += cellHTML; + } + + html += cellsHTML; + html += + " " + + "" + + ""; + + return html; + } + + + // TODO: data-date on the cells + + + + /* Dimensions + -----------------------------------------------------------------------*/ + + + function setHeight(height) { + if (height === undefined) { + height = viewHeight; + } + viewHeight = height; + slotTopCache = {}; + + var headHeight = dayBody.position().top; + var allDayHeight = slotScroller.position().top; // including divider + var bodyHeight = Math.min( // total body height, including borders + height - headHeight, // when scrollbars + slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border + ); + + dayBodyFirstCellStretcher + .height(bodyHeight - vsides(dayBodyFirstCell)); + + slotLayer.css('top', headHeight); + + slotScroller.height(bodyHeight - allDayHeight - 1); + + // the stylesheet guarantees that the first row has no border. + // this allows .height() to work well cross-browser. + slotHeight = slotTable.find('tr:first').height() + 1; // +1 for bottom border + + snapRatio = opt('slotMinutes') / snapMinutes; + snapHeight = slotHeight / snapRatio; + } + + + function setWidth(width) { + viewWidth = width; + colPositions.clear(); + colContentPositions.clear(); + + var axisFirstCells = dayHead.find('th:first'); + if (allDayTable) { + axisFirstCells = axisFirstCells.add(allDayTable.find('th:first')); + } + axisFirstCells = axisFirstCells.add(slotTable.find('th:first')); + + axisWidth = 0; + setOuterWidth( + axisFirstCells + .width('') + .each(function(i, _cell) { + axisWidth = Math.max(axisWidth, $(_cell).outerWidth()); + }), + axisWidth + ); + + var gutterCells = dayTable.find('.fc-agenda-gutter'); + if (allDayTable) { + gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter')); + } + + var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7) + + gutterWidth = slotScroller.width() - slotTableWidth; + if (gutterWidth) { + setOuterWidth(gutterCells, gutterWidth); + gutterCells + .show() + .prev() + .removeClass('fc-last'); + }else{ + gutterCells + .hide() + .prev() + .addClass('fc-last'); + } + + colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt); + setOuterWidth(dayHeadCells.slice(0, -1), colWidth); + } + + + + /* Scrolling + -----------------------------------------------------------------------*/ + + + function resetScroll() { + var d0 = zeroDate(); + var scrollDate = cloneDate(d0); + scrollDate.setHours(opt('firstHour')); + var top = timePosition(d0, scrollDate) + 1; // +1 for the border + function scroll() { + slotScroller.scrollTop(top); + } + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + } + + + function afterRender() { // after the view has been freshly rendered and sized + resetScroll(); + } + + + + /* Slot/Day clicking and binding + -----------------------------------------------------------------------*/ + + + function dayBind(cells) { + cells.click(slotClick) + .mousedown(daySelectionMousedown); + } + + + function slotBind(cells) { + cells.click(slotClick) + .mousedown(slotSelectionMousedown); + } + + + function slotClick(ev) { + if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick + var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth)); + var date = cellToDate(0, col); + var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data + if (rowMatch) { + var mins = parseInt(rowMatch[1]) * opt('slotMinutes'); + var hours = Math.floor(mins/60); + date.setHours(hours); + date.setMinutes(mins%60 + minMinute); + trigger('dayClick', dayBodyCells[col], date, false, ev); + }else{ + trigger('dayClick', dayBodyCells[col], date, true, ev); + } + } + } + + + + /* Semi-transparent Overlay Helpers + -----------------------------------------------------*/ + // TODO: should be consolidated with BasicView's methods + + + function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive + + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + + var segments = rangeToSegments(overlayStart, overlayEnd); + + for (var i=0; i= 0) { + addMinutes(d, minMinute + slotIndex * snapMinutes); + } + return d; + } + + + // get the Y coordinate of the given time on the given day (both Date objects) + function timePosition(day, time) { // both date objects. day holds 00:00 of current day + day = cloneDate(day, true); + if (time < addMinutes(cloneDate(day), minMinute)) { + return 0; + } + if (time >= addMinutes(cloneDate(day), maxMinute)) { + return slotTable.height(); + } + var slotMinutes = opt('slotMinutes'), + minutes = time.getHours()*60 + time.getMinutes() - minMinute, + slotI = Math.floor(minutes / slotMinutes), + slotTop = slotTopCache[slotI]; + if (slotTop === undefined) { + slotTop = slotTopCache[slotI] = + slotTable.find('tr').eq(slotI).find('td div')[0].offsetTop; + // .eq() is faster than ":eq()" selector + // [0].offsetTop is faster than .position().top (do we really need this optimization?) + // a better optimization would be to cache all these divs + } + return Math.max(0, Math.round( + slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) + )); + } + + + function getAllDayRow(index) { + return allDayRow; + } + + + function defaultEventEnd(event) { + var start = cloneDate(event.start); + if (event.allDay) { + return start; + } + return addMinutes(start, opt('defaultEventMinutes')); + } + + + + /* Selection + ---------------------------------------------------------------------------------*/ + + + function defaultSelectionEnd(startDate, allDay) { + if (allDay) { + return cloneDate(startDate); + } + return addMinutes(cloneDate(startDate), opt('slotMinutes')); + } + + + function renderSelection(startDate, endDate, allDay) { // only for all-day + if (allDay) { + if (opt('allDaySlot')) { + renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); + } + }else{ + renderSlotSelection(startDate, endDate); + } + } + + + function renderSlotSelection(startDate, endDate) { + var helperOption = opt('selectHelper'); + coordinateGrid.build(); + if (helperOption) { + var col = dateToCell(startDate).col; + if (col >= 0 && col < colCnt) { // only works when times are on same day + var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords + var top = timePosition(startDate, startDate); + var bottom = timePosition(startDate, endDate); + if (bottom > top) { // protect against selections that are entirely before or after visible range + rect.top = top; + rect.height = bottom - top; + rect.left += 2; + rect.width -= 5; + if ($.isFunction(helperOption)) { + var helperRes = helperOption(startDate, endDate); + if (helperRes) { + rect.position = 'absolute'; + selectionHelper = $(helperRes) + .css(rect) + .appendTo(slotContainer); + } + }else{ + rect.isStart = true; // conside rect a "seg" now + rect.isEnd = true; // + selectionHelper = $(slotSegHtml( + { + title: '', + start: startDate, + end: endDate, + className: ['fc-select-helper'], + editable: false + }, + rect + )); + selectionHelper.css('opacity', opt('dragOpacity')); + } + if (selectionHelper) { + slotBind(selectionHelper); + slotContainer.append(selectionHelper); + setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended + setOuterHeight(selectionHelper, rect.height, true); + } + } + } + }else{ + renderSlotOverlay(startDate, endDate); + } + } + + + function clearSelection() { + clearOverlays(); + if (selectionHelper) { + selectionHelper.remove(); + selectionHelper = null; + } + } + + + function slotSelectionMousedown(ev) { + if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button + unselect(ev); + var dates; + hoverListener.start(function(cell, origCell) { + clearSelection(); + if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) { + var d1 = realCellToDate(origCell); + var d2 = realCellToDate(cell); + dates = [ + d1, + addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes + d2, + addMinutes(cloneDate(d2), snapMinutes) + ].sort(dateCompare); + renderSlotSelection(dates[0], dates[3]); + }else{ + dates = null; + } + }, ev); + $(document).one('mouseup', function(ev) { + hoverListener.stop(); + if (dates) { + if (+dates[0] == +dates[1]) { + reportDayClick(dates[0], false, ev); + } + reportSelection(dates[0], dates[3], false, ev); + } + }); + } + } + + + function reportDayClick(date, allDay, ev) { + trigger('dayClick', dayBodyCells[dateToCell(date).col], date, allDay, ev); + } + + + + /* External Dragging + --------------------------------------------------------------------------------*/ + + + function dragStart(_dragElement, ev, ui) { + hoverListener.start(function(cell) { + clearOverlays(); + if (cell) { + if (getIsCellAllDay(cell)) { + renderCellOverlay(cell.row, cell.col, cell.row, cell.col); + }else{ + var d1 = realCellToDate(cell); + var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes')); + renderSlotOverlay(d1, d2); + } + } + }, ev); + } + + + function dragStop(_dragElement, ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + if (cell) { + trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui); + } + } + + +} + +;; + +function AgendaEventRenderer() { + var t = this; + + + // exports + t.renderEvents = renderEvents; + t.clearEvents = clearEvents; + t.slotSegHtml = slotSegHtml; + + + // imports + DayEventRenderer.call(t); + var opt = t.opt; + var trigger = t.trigger; + var isEventDraggable = t.isEventDraggable; + var isEventResizable = t.isEventResizable; + var eventEnd = t.eventEnd; + var eventElementHandlers = t.eventElementHandlers; + var setHeight = t.setHeight; + var getDaySegmentContainer = t.getDaySegmentContainer; + var getSlotSegmentContainer = t.getSlotSegmentContainer; + var getHoverListener = t.getHoverListener; + var getMaxMinute = t.getMaxMinute; + var getMinMinute = t.getMinMinute; + var timePosition = t.timePosition; + var getIsCellAllDay = t.getIsCellAllDay; + var colContentLeft = t.colContentLeft; + var colContentRight = t.colContentRight; + var cellToDate = t.cellToDate; + var getColCnt = t.getColCnt; + var getColWidth = t.getColWidth; + var getSnapHeight = t.getSnapHeight; + var getSnapMinutes = t.getSnapMinutes; + var getSlotContainer = t.getSlotContainer; + var reportEventElement = t.reportEventElement; + var showEvents = t.showEvents; + var hideEvents = t.hideEvents; + var eventDrop = t.eventDrop; + var eventResize = t.eventResize; + var renderDayOverlay = t.renderDayOverlay; + var clearOverlays = t.clearOverlays; + var renderDayEvents = t.renderDayEvents; + var calendar = t.calendar; + var formatDate = calendar.formatDate; + var formatDates = calendar.formatDates; + + + // overrides + t.draggableDayEvent = draggableDayEvent; + + + + /* Rendering + ----------------------------------------------------------------------------*/ + + + function renderEvents(events, modifiedEventId) { + var i, len=events.length, + dayEvents=[], + slotEvents=[]; + for (i=0; i start && eventStart < end) { + if (eventStart < start) { + segStart = cloneDate(start); + isStart = false; + }else{ + segStart = eventStart; + isStart = true; + } + if (eventEnd > end) { + segEnd = cloneDate(end); + isEnd = false; + }else{ + segEnd = eventEnd; + isEnd = true; + } + segs.push({ + event: event, + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd + }); + } + } + return segs.sort(compareSlotSegs); + } + + + function slotEventEnd(event) { + if (event.end) { + return cloneDate(event.end); + }else{ + return addMinutes(cloneDate(event.start), opt('defaultEventMinutes')); + } + } + + + // renders events in the 'time slots' at the bottom + // TODO: when we refactor this, when user returns `false` eventRender, don't have empty space + // TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp) + + function renderSlotSegs(segs, modifiedEventId) { + + var i, segCnt=segs.length, seg, + event, + top, + bottom, + columnLeft, + columnRight, + columnWidth, + width, + left, + right, + html = '', + eventElements, + eventElement, + triggerRes, + titleElement, + height, + slotSegmentContainer = getSlotSegmentContainer(), + isRTL = opt('isRTL'); + + // calculate position/dimensions, create html + for (i=0; i" + + "
    " + + "
    " + + htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + + "
    " + + "
    " + + htmlEscape(event.title || '') + + "
    " + + "
    " + + "
    "; + if (seg.isEnd && isEventResizable(event)) { + html += + "
    =
    "; + } + html += + ""; + return html; + } + + + function bindSlotSeg(event, eventElement, seg) { + var timeElement = eventElement.find('div.fc-event-time'); + if (isEventDraggable(event)) { + draggableSlotEvent(event, eventElement, timeElement); + } + if (seg.isEnd && isEventResizable(event)) { + resizableSlotEvent(event, eventElement, timeElement); + } + eventElementHandlers(event, eventElement); + } + + + + /* Dragging + -----------------------------------------------------------------------------------*/ + + + // when event starts out FULL-DAY + // overrides DayEventRenderer's version because it needs to account for dragging elements + // to and from the slot area. + + function draggableDayEvent(event, eventElement, seg) { + var isStart = seg.isStart; + var origWidth; + var revert; + var allDay = true; + var dayDelta; + var hoverListener = getHoverListener(); + var colWidth = getColWidth(); + var snapHeight = getSnapHeight(); + var snapMinutes = getSnapMinutes(); + var minMinute = getMinMinute(); + eventElement.draggable({ + opacity: opt('dragOpacity', 'month'), // use whatever the month view was using + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); + origWidth = eventElement.width(); + hoverListener.start(function(cell, origCell) { + clearOverlays(); + if (cell) { + revert = false; + var origDate = cellToDate(0, origCell.col); + var date = cellToDate(0, cell.col); + dayDelta = dayDiff(date, origDate); + if (!cell.row) { + // on full-days + renderDayOverlay( + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + resetElement(); + }else{ + // mouse is over bottom slots + if (isStart) { + if (allDay) { + // convert event to temporary slot-event + eventElement.width(colWidth - 10); // don't use entire width + setOuterHeight( + eventElement, + snapHeight * Math.round( + (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) / + snapMinutes + ) + ); + eventElement.draggable('option', 'grid', [colWidth, 1]); + allDay = false; + } + }else{ + revert = true; + } + } + revert = revert || (allDay && !dayDelta); + }else{ + resetElement(); + revert = true; + } + eventElement.draggable('option', 'revert', revert); + }, ev, 'drag'); + }, + stop: function(ev, ui) { + hoverListener.stop(); + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); + if (revert) { + // hasn't moved or is out of bounds (draggable has already reverted) + resetElement(); + eventElement.css('filter', ''); // clear IE opacity side-effects + showEvents(event, eventElement); + }else{ + // changed! + var minuteDelta = 0; + if (!allDay) { + minuteDelta = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight) + * snapMinutes + + minMinute + - (event.start.getHours() * 60 + event.start.getMinutes()); + } + eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui); + } + } + }); + function resetElement() { + if (!allDay) { + eventElement + .width(origWidth) + .height('') + .draggable('option', 'grid', null); + allDay = true; + } + } + } + + + // when event starts out IN TIMESLOTS + + function draggableSlotEvent(event, eventElement, timeElement) { + var coordinateGrid = t.getCoordinateGrid(); + var colCnt = getColCnt(); + var colWidth = getColWidth(); + var snapHeight = getSnapHeight(); + var snapMinutes = getSnapMinutes(); + + // states + var origPosition; // original position of the element, not the mouse + var origCell; + var isInBounds, prevIsInBounds; + var isAllDay, prevIsAllDay; + var colDelta, prevColDelta; + var dayDelta; // derived from colDelta + var minuteDelta, prevMinuteDelta; + + eventElement.draggable({ + scroll: false, + grid: [ colWidth, snapHeight ], + axis: colCnt==1 ? 'y' : false, + opacity: opt('dragOpacity'), + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); + + coordinateGrid.build(); + + // initialize states + origPosition = eventElement.position(); + origCell = coordinateGrid.cell(ev.pageX, ev.pageY); + isInBounds = prevIsInBounds = true; + isAllDay = prevIsAllDay = getIsCellAllDay(origCell); + colDelta = prevColDelta = 0; + dayDelta = 0; + minuteDelta = prevMinuteDelta = 0; + + }, + drag: function(ev, ui) { + + // NOTE: this `cell` value is only useful for determining in-bounds and all-day. + // Bad for anything else due to the discrepancy between the mouse position and the + // element position while snapping. (problem revealed in PR #55) + // + // PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event. + // We should overhaul the dragging system and stop relying on jQuery UI. + var cell = coordinateGrid.cell(ev.pageX, ev.pageY); + + // update states + isInBounds = !!cell; + if (isInBounds) { + isAllDay = getIsCellAllDay(cell); + + // calculate column delta + colDelta = Math.round((ui.position.left - origPosition.left) / colWidth); + if (colDelta != prevColDelta) { + // calculate the day delta based off of the original clicked column and the column delta + var origDate = cellToDate(0, origCell.col); + var col = origCell.col + colDelta; + col = Math.max(0, col); + col = Math.min(colCnt-1, col); + var date = cellToDate(0, col); + dayDelta = dayDiff(date, origDate); + } + + // calculate minute delta (only if over slots) + if (!isAllDay) { + minuteDelta = Math.round((ui.position.top - origPosition.top) / snapHeight) * snapMinutes; + } + } + + // any state changes? + if ( + isInBounds != prevIsInBounds || + isAllDay != prevIsAllDay || + colDelta != prevColDelta || + minuteDelta != prevMinuteDelta + ) { + + updateUI(); + + // update previous states for next time + prevIsInBounds = isInBounds; + prevIsAllDay = isAllDay; + prevColDelta = colDelta; + prevMinuteDelta = minuteDelta; + } + + // if out-of-bounds, revert when done, and vice versa. + eventElement.draggable('option', 'revert', !isInBounds); + + }, + stop: function(ev, ui) { + + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); + + if (isInBounds && (isAllDay || dayDelta || minuteDelta)) { // changed! + eventDrop(this, event, dayDelta, isAllDay ? 0 : minuteDelta, isAllDay, ev, ui); + } + else { // either no change or out-of-bounds (draggable has already reverted) + + // reset states for next time, and for updateUI() + isInBounds = true; + isAllDay = false; + colDelta = 0; + dayDelta = 0; + minuteDelta = 0; + + updateUI(); + eventElement.css('filter', ''); // clear IE opacity side-effects + + // sometimes fast drags make event revert to wrong position, so reset. + // also, if we dragged the element out of the area because of snapping, + // but the *mouse* is still in bounds, we need to reset the position. + eventElement.css(origPosition); + + showEvents(event, eventElement); + } + } + }); + + function updateUI() { + clearOverlays(); + if (isInBounds) { + if (isAllDay) { + timeElement.hide(); + eventElement.draggable('option', 'grid', null); // disable grid snapping + renderDayOverlay( + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + } + else { + updateTimeText(minuteDelta); + timeElement.css('display', ''); // show() was causing display=inline + eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping + } + } + } + + function updateTimeText(minuteDelta) { + var newStart = addMinutes(cloneDate(event.start), minuteDelta); + var newEnd; + if (event.end) { + newEnd = addMinutes(cloneDate(event.end), minuteDelta); + } + timeElement.text(formatDates(newStart, newEnd, opt('timeFormat'))); + } + + } + + + + /* Resizing + --------------------------------------------------------------------------------------*/ + + + function resizableSlotEvent(event, eventElement, timeElement) { + var snapDelta, prevSnapDelta; + var snapHeight = getSnapHeight(); + var snapMinutes = getSnapMinutes(); + eventElement.resizable({ + handles: { + s: '.ui-resizable-handle' + }, + grid: snapHeight, + start: function(ev, ui) { + snapDelta = prevSnapDelta = 0; + hideEvents(event, eventElement); + trigger('eventResizeStart', this, event, ev, ui); + }, + resize: function(ev, ui) { + // don't rely on ui.size.height, doesn't take grid into account + snapDelta = Math.round((Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) / snapHeight); + if (snapDelta != prevSnapDelta) { + timeElement.text( + formatDates( + event.start, + (!snapDelta && !event.end) ? null : // no change, so don't display time range + addMinutes(eventEnd(event), snapMinutes*snapDelta), + opt('timeFormat') + ) + ); + prevSnapDelta = snapDelta; + } + }, + stop: function(ev, ui) { + trigger('eventResizeStop', this, event, ev, ui); + if (snapDelta) { + eventResize(this, event, 0, snapMinutes*snapDelta, ev, ui); + }else{ + showEvents(event, eventElement); + // BUG: if event was really short, need to put title back in span + } + } + }); + } + + +} + + + +/* Agenda Event Segment Utilities +-----------------------------------------------------------------------------*/ + + +// Sets the seg.backwardCoord and seg.forwardCoord on each segment and returns a new +// list in the order they should be placed into the DOM (an implicit z-index). +function placeSlotSegs(segs) { + var levels = buildSlotSegLevels(segs); + var level0 = levels[0]; + var i; + + computeForwardSlotSegs(levels); + + if (level0) { + + for (i=0; i seg2.start && seg1.start < seg2.end; +} + + +// A cmp function for determining which forward segment to rely on more when computing coordinates. +function compareForwardSlotSegs(seg1, seg2) { + // put higher-pressure first + return seg2.forwardPressure - seg1.forwardPressure || + // put segments that are closer to initial edge first (and favor ones with no coords yet) + (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || + // do normal sorting... + compareSlotSegs(seg1, seg2); +} + + +// A cmp function for determining which segment should be closer to the initial edge +// (the left edge on a left-to-right calendar). +function compareSlotSegs(seg1, seg2) { + return seg1.start - seg2.start || // earlier start time goes first + (seg2.end - seg2.start) - (seg1.end - seg1.start) || // tie? longer-duration goes first + (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title +} + + +;; + + +function View(element, calendar, viewName) { + var t = this; + + + // exports + t.element = element; + t.calendar = calendar; + t.name = viewName; + t.opt = opt; + t.trigger = trigger; + t.isEventDraggable = isEventDraggable; + t.isEventResizable = isEventResizable; + t.setEventData = setEventData; + t.clearEventData = clearEventData; + t.eventEnd = eventEnd; + t.reportEventElement = reportEventElement; + t.triggerEventDestroy = triggerEventDestroy; + t.eventElementHandlers = eventElementHandlers; + t.showEvents = showEvents; + t.hideEvents = hideEvents; + t.eventDrop = eventDrop; + t.eventResize = eventResize; + // t.title + // t.start, t.end + // t.visStart, t.visEnd + + + // imports + var defaultEventEnd = t.defaultEventEnd; + var normalizeEvent = calendar.normalizeEvent; // in EventManager + var reportEventChange = calendar.reportEventChange; + + + // locals + var eventsByID = {}; // eventID mapped to array of events (there can be multiple b/c of repeating events) + var eventElementsByID = {}; // eventID mapped to array of jQuery elements + var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system + var options = calendar.options; + + + + function opt(name, viewNameOverride) { + var v = options[name]; + if ($.isPlainObject(v)) { + return smartProperty(v, viewNameOverride || viewName); + } + return v; + } + + + function trigger(name, thisObj) { + return calendar.trigger.apply( + calendar, + [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) + ); + } + + + + /* Event Editable Boolean Calculations + ------------------------------------------------------------------------------*/ + + + function isEventDraggable(event) { + var source = event.source || {}; + return firstDefined( + event.startEditable, + source.startEditable, + opt('eventStartEditable'), + event.editable, + source.editable, + opt('editable') + ) + && !opt('disableDragging'); // deprecated + } + + + function isEventResizable(event) { // but also need to make sure the seg.isEnd == true + var source = event.source || {}; + return firstDefined( + event.durationEditable, + source.durationEditable, + opt('eventDurationEditable'), + event.editable, + source.editable, + opt('editable') + ) + && !opt('disableResizing'); // deprecated + } + + + + /* Event Data + ------------------------------------------------------------------------------*/ + + + function setEventData(events) { // events are already normalized at this point + eventsByID = {}; + var i, len=events.length, event; + for (i=0; i