# HG changeset patch # User Denis Laxalde # Date 1455713134 -3600 # Node ID 97095348b3eeeb28ba3a0e6ac1fe98348fd41c2d # Parent 9b4de34ad3946b27af52add19c658e68a2e10426# Parent 6464edfa95bbcc4ca9a9f24759393a8fdaf0131d Merge with 3.22 branch The merge was clean, just dropped cubicweb/web/data/cubicweb.goa.js. diff -r 9b4de34ad394 -r 97095348b3ee .hgtags --- a/.hgtags Thu Feb 11 21:59:49 2016 +0100 +++ b/.hgtags Wed Feb 17 13:45:34 2016 +0100 @@ -472,6 +472,9 @@ f933a38d7ab5fc6f2ad593fe1cf9985ce9d7e873 3.19.13 f933a38d7ab5fc6f2ad593fe1cf9985ce9d7e873 debian/3.19.13-1 f933a38d7ab5fc6f2ad593fe1cf9985ce9d7e873 centos/3.19.13-1 +72a0f70879ac40ea57575be90bc6427f61ce3bd6 3.19.14 +72a0f70879ac40ea57575be90bc6427f61ce3bd6 debian/3.19.14-1 +72a0f70879ac40ea57575be90bc6427f61ce3bd6 centos/3.19.14-1 7e6b7739afe6128589ad51b0318decb767cbae36 3.20.0 7e6b7739afe6128589ad51b0318decb767cbae36 debian/3.20.0-1 7e6b7739afe6128589ad51b0318decb767cbae36 centos/3.20.0-1 @@ -511,6 +514,9 @@ 03e8fc9f79a6e489a1b5c695eb0cd3fbb1afe9d4 3.20.12 03e8fc9f79a6e489a1b5c695eb0cd3fbb1afe9d4 debian/3.20.12-1 03e8fc9f79a6e489a1b5c695eb0cd3fbb1afe9d4 centos/3.20.12-1 +8c5dabbcd4d9505c3a617f9dbe2b10172bdc2b3a 3.20.13 +8c5dabbcd4d9505c3a617f9dbe2b10172bdc2b3a debian/3.20.13-1 +8c5dabbcd4d9505c3a617f9dbe2b10172bdc2b3a centos/3.20.13-1 887c6eef807781560adcd4ecd2dea9011f5a6681 3.21.0 887c6eef807781560adcd4ecd2dea9011f5a6681 debian/3.21.0-1 887c6eef807781560adcd4ecd2dea9011f5a6681 centos/3.21.0-1 @@ -530,6 +536,12 @@ e0572a786e6b4b0965d405dd95cf5bce754005a2 debian/3.21.5-1 e0572a786e6b4b0965d405dd95cf5bce754005a2 centos/3.21.5-1 228b6d2777e44d7bc158d0b4579d09960acea926 debian/3.21.5-2 +b3cbbb7690b6e193570ffe4846615d372868a923 3.21.6 +b3cbbb7690b6e193570ffe4846615d372868a923 debian/3.21.6-1 +b3cbbb7690b6e193570ffe4846615d372868a923 centos/3.21.6-1 de472896fc0a18d6b831e6fed0eeda5921ec522c 3.22.0 de472896fc0a18d6b831e6fed0eeda5921ec522c debian/3.22.0-1 de472896fc0a18d6b831e6fed0eeda5921ec522c centos/3.22.0-1 +d0d86803a804854be0a1b2d49079a94d1c193ee9 3.22.1 +d0d86803a804854be0a1b2d49079a94d1c193ee9 debian/3.22.1-1 +d0d86803a804854be0a1b2d49079a94d1c193ee9 centos/3.22.1-1 diff -r 9b4de34ad394 -r 97095348b3ee cubicweb.spec --- a/cubicweb.spec Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb.spec Wed Feb 17 13:45:34 2016 +0100 @@ -5,9 +5,10 @@ %define python python %define __python /usr/bin/python %endif +%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} Name: cubicweb -Version: 3.22.0 +Version: 3.22.1 Release: logilab.1%{?dist} Summary: CubicWeb is a semantic web application framework Source0: http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz @@ -30,7 +31,7 @@ Requires: %{python}-lxml Requires: %{python}-twisted-web Requires: %{python}-markdown -Requires: %{python}-tz +Requires: pytz # the schema view uses `dot'; at least on el5, png output requires graphviz-gd Requires: graphviz-gd Requires: gettext @@ -57,5 +58,6 @@ %files %defattr(-, root, root) %dir /var/log/cubicweb -/* - +%{_prefix}/share/cubicweb +%{python_sitelib} +%{_bindir} diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/__init__.py --- a/cubicweb/__init__.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/__init__.py Wed Feb 17 13:45:34 2016 +0100 @@ -34,7 +34,11 @@ CW_SOFTWARE_ROOT = __path__[0] import sys, os, logging -from io import BytesIO +if (2, 7) <= sys.version_info < (2, 7, 4): + # http://bugs.python.org/issue10211 + from StringIO import StringIO as BytesIO +else: + from io import BytesIO from six.moves import cPickle as pickle @@ -79,12 +83,14 @@ def __init__(self, buf=b''): assert isinstance(buf, self._allowed_types), \ "Binary objects must use bytes/buffer objects, not %s" % buf.__class__ - super(Binary, self).__init__(buf) + # don't call super, BytesIO may be an old-style class (on python < 2.7.4) + BytesIO.__init__(self, buf) def write(self, data): assert isinstance(data, self._allowed_types), \ "Binary objects must use bytes/buffer objects, not %s" % data.__class__ - super(Binary, self).write(data) + # don't call super, BytesIO may be an old-style class (on python < 2.7.4) + BytesIO.write(self, data) def to_file(self, fobj): """write a binary to disk diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/__pkginfo__.py --- a/cubicweb/__pkginfo__.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/__pkginfo__.py Wed Feb 17 13:45:34 2016 +0100 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 22, 0) +numversion = (3, 22, 1) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/_exceptions.py --- a/cubicweb/_exceptions.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/_exceptions.py Wed Feb 17 13:45:34 2016 +0100 @@ -117,19 +117,19 @@ """raised when a user tries to perform an action without sufficient credentials """ - msg = 'You are not allowed to perform this operation' - msg1 = 'You are not allowed to perform %s operation on %s' + msg = u'You are not allowed to perform this operation' + msg1 = u'You are not allowed to perform %s operation on %s' var = None - def __str__(self): + def __unicode__(self): try: if self.args and len(self.args) == 2: return self.msg1 % self.args if self.args: - return ' '.join(self.args) + return u' '.join(self.args) return self.msg except Exception as ex: - return str(ex) + return text_type(ex) class Forbidden(SecurityError): """raised when a user tries to perform a forbidden action diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/cwconfig.py --- a/cubicweb/cwconfig.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/cwconfig.py Wed Feb 17 13:45:34 2016 +0100 @@ -628,7 +628,7 @@ cls.cls_adjust_sys_path() for ctlfile in ('web/webctl.py', 'etwist/twctl.py', 'server/serverctl.py', - 'devtools/devctl.py', 'goa/goactl.py'): + 'devtools/devctl.py',): if exists(join(CW_SOFTWARE_ROOT, ctlfile)): try: load_module_from_file(join(CW_SOFTWARE_ROOT, ctlfile)) diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/cwctl.py --- a/cubicweb/cwctl.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/cwctl.py Wed Feb 17 13:45:34 2016 +0100 @@ -43,6 +43,7 @@ from logilab.common.clcommands import CommandLine from logilab.common.shellutils import ASK from logilab.common.configuration import merge_options +from logilab.common.deprecation import deprecated from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage from cubicweb.utils import support_args @@ -103,38 +104,19 @@ ) actionverb = None + @deprecated('[3.22] startorder is not used any more') def ordered_instances(self): - """return instances in the order in which they should be started, - considering $REGISTRY_DIR/startorder file if it exists (useful when - some instances depends on another as external source). - - Instance used by another one should appears first in the file (one - instance per line) + """return list of known instances """ regdir = cwcfg.instances_dir() - _allinstances = list_instances(regdir) - if isfile(join(regdir, 'startorder')): - allinstances = [] - for line in open(join(regdir, 'startorder')): - line = line.strip() - if line and not line.startswith('#'): - try: - _allinstances.remove(line) - allinstances.append(line) - except ValueError: - print('ERROR: startorder file contains unexistant ' - 'instance %s' % line) - allinstances += _allinstances - else: - allinstances = _allinstances - return allinstances + return list_instances(regdir) def run(self, args): """run the _method on each argument (a list of instance identifiers) """ if not args: - args = self.ordered_instances() + args = list_instances(cwcfg.instances_dir()) try: askconfirm = not self.config.force except AttributeError: @@ -572,11 +554,6 @@ name = 'stop' actionverb = 'stopped' - def ordered_instances(self): - instances = super(StopInstanceCommand, self).ordered_instances() - instances.reverse() - return instances - def stop_instance(self, appid): """stop the instance's server""" config = cwcfg.config_for(appid) @@ -621,29 +598,6 @@ name = 'restart' actionverb = 'restarted' - def run_args(self, args, askconfirm): - regdir = cwcfg.instances_dir() - if not isfile(join(regdir, 'startorder')) or len(args) <= 1: - # no specific startorder - super(RestartInstanceCommand, self).run_args(args, askconfirm) - return - print ('some specific start order is specified, will first stop all ' - 'instances then restart them.') - # get instances in startorder - for appid in args: - if askconfirm: - print('*'*72) - if not ASK.confirm('%s instance %r ?' % (self.name, appid)): - continue - StopInstanceCommand(self.logger).stop_instance(appid) - forkcmd = [w for w in sys.argv if not w in args] - forkcmd[1] = 'start' - forkcmd = ' '.join(forkcmd) - for appid in reversed(args): - status = system('%s %s' % (forkcmd, appid)) - if status: - sys.exit(status) - def restart_instance(self, appid): StopInstanceCommand(self.logger).stop_instance(appid) self.start_instance(appid) diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/dataimport/pgstore.py --- a/cubicweb/dataimport/pgstore.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/dataimport/pgstore.py Wed Feb 17 13:45:34 2016 +0100 @@ -30,7 +30,6 @@ from six.moves import cPickle as pickle, range from cubicweb.utils import make_uid -from cubicweb.server.utils import eschema_eid from cubicweb.server.sqlutils import SQL_PREFIX from cubicweb.dataimport.stores import NoHookRQLObjectStore @@ -425,20 +424,14 @@ 'asource': source.uri} self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs) # insert core relations: is, is_instance_of and cw_source - try: - self._handle_is_relation_sql(cnx, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)', - (entity.eid, eschema_eid(cnx, entity.e_schema))) - except IndexError: - # during schema serialization, skip - pass - else: - for eschema in entity.e_schema.ancestors() + [entity.e_schema]: - self._handle_is_relation_sql(cnx, - 'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)', - (entity.eid, eschema_eid(cnx, eschema))) - if 'CWSource' in self.schema and source.eid is not None: # else, cw < 3.10 - self._handle_is_relation_sql(cnx, 'INSERT INTO cw_source_relation(eid_from,eid_to) VALUES (%s,%s)', - (entity.eid, source.eid)) + self._handle_is_relation_sql(cnx, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)', + (entity.eid, entity.e_schema.eid)) + for eschema in entity.e_schema.ancestors() + [entity.e_schema]: + self._handle_is_relation_sql(cnx, + 'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)', + (entity.eid, eschema.eid)) + self._handle_is_relation_sql(cnx, 'INSERT INTO cw_source_relation(eid_from,eid_to) VALUES (%s,%s)', + (entity.eid, source.eid)) # now we can update the full text index if self.do_fti and self.need_fti_indexation(entity.cw_etype): self.index_entity(cnx, entity=entity) diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/dataimport/test/unittest_importer.py --- a/cubicweb/dataimport/test/unittest_importer.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/dataimport/test/unittest_importer.py Wed Feb 17 13:45:34 2016 +0100 @@ -132,7 +132,7 @@ importer = self.importer(cnx) # First import richelieu = ExtEntity('Personne', 11, - {'nom': {u'Richelieu Diacre'}}) + {'nom': set([u'Richelieu Diacre'])}) importer.import_entities([richelieu]) cnx.commit() rset = cnx.execute('Any X WHERE X is Personne') @@ -140,7 +140,7 @@ self.assertEqual(entity.nom, u'Richelieu Diacre') # Second import richelieu = ExtEntity('Personne', 11, - {'nom': {u'Richelieu Cardinal'}}) + {'nom': set([u'Richelieu Cardinal'])}) importer.import_entities([richelieu]) cnx.commit() rset = cnx.execute('Any X WHERE X is Personne') diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/devtools/__init__.py --- a/cubicweb/devtools/__init__.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/devtools/__init__.py Wed Feb 17 13:45:34 2016 +0100 @@ -405,7 +405,7 @@ """Factory method to create a new Repository Instance""" config._cubes = None repo = config.repository() - config.repository = lambda x=None: repo + config.repository = lambda vreg=None: repo # extending Repository class repo._has_started = False repo._needs_refresh = False diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/devtools/devctl.py --- a/cubicweb/devtools/devctl.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/devtools/devctl.py Wed Feb 17 13:45:34 2016 +0100 @@ -44,6 +44,11 @@ from cubicweb.server.serverconfig import ServerConfiguration +STD_BLACKLIST = set(STD_BLACKLIST) +STD_BLACKLIST.add('.tox') +STD_BLACKLIST.add('test') + + class DevConfiguration(ServerConfiguration, WebConfiguration): """dummy config to get full library schema and appobjects for a cube or for cubicweb (without a home) @@ -453,7 +458,7 @@ schemapotstream.close() print('TAL', end=' ') tali18nfile = osp.join(tempdir, 'tali18n.py') - ptfiles = find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST+('test',)) + ptfiles = find('.', ('.py', '.pt'), blacklist=STD_BLACKLIST) extract_from_tal(ptfiles, tali18nfile) print('Javascript') jsfiles = [jsfile for jsfile in find('.', '.js') @@ -468,7 +473,7 @@ potfiles.append(tmppotfile) print('-> creating cube-specific catalog') tmppotfile = osp.join(tempdir, 'generated.pot') - cubefiles = find('.', '.py', blacklist=STD_BLACKLIST+('test',)) + cubefiles = find('.', '.py', blacklist=STD_BLACKLIST) cubefiles.append(tali18nfile) cmd = ['xgettext', '--no-location', '--omit-header', '-k_', '-o', tmppotfile] cmd.extend(cubefiles) diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/devtools/fill.py --- a/cubicweb/devtools/fill.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/devtools/fill.py Wed Feb 17 13:45:34 2016 +0100 @@ -42,6 +42,9 @@ from cubicweb.schema import RQLConstraint def custom_range(start, stop, step): + if start == stop: + yield start + return while start < stop: yield start start += step @@ -214,8 +217,10 @@ minvalue = maxvalue = None for cst in self.eschema.rdef(attrname).constraints: if isinstance(cst, IntervalBoundConstraint): - minvalue = self._actual_boundary(entity, attrname, cst.minvalue) - maxvalue = self._actual_boundary(entity, attrname, cst.maxvalue) + if cst.minvalue is not None: + minvalue = self._actual_boundary(entity, attrname, cst.minvalue) + if cst.maxvalue is not None: + maxvalue = self._actual_boundary(entity, attrname, cst.maxvalue) elif isinstance(cst, BoundaryConstraint): if cst.operator[0] == '<': maxvalue = self._actual_boundary(entity, attrname, cst.boundary) diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/devtools/testlib.py --- a/cubicweb/devtools/testlib.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/devtools/testlib.py Wed Feb 17 13:45:34 2016 +0100 @@ -318,7 +318,6 @@ """return admin session""" return self._admin_session - # XXX this doesn't need to a be classmethod anymore def _init_repo(self): """init the repository and connection to it. """ diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/entities/adapters.py --- a/cubicweb/entities/adapters.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/entities/adapters.py Wed Feb 17 13:45:34 2016 +0100 @@ -423,5 +423,9 @@ else: assert 0 key = rschema.type + '-subject' - msg, args = constraint.failed_message(key, self.entity.cw_edited[rschema.type]) + # use .get since a constraint may be associated to an attribute that isn't edited (e.g. + # constraint between two attributes). This should be the purpose of an api rework at some + # point, we currently rely on the fact that such constraint will provide a dedicated user + # message not relying on the `value` argument + msg, args = constraint.failed_message(key, self.entity.cw_edited.get(rschema.type)) raise ValidationError(self.entity.eid, {key: msg}, args) diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/entity.py --- a/cubicweb/entity.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/entity.py Wed Feb 17 13:45:34 2016 +0100 @@ -20,7 +20,6 @@ __docformat__ = "restructuredtext en" from warnings import warn -from functools import partial from six import text_type, string_types, integer_types from six.moves import range diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/hooks/notification.py --- a/cubicweb/hooks/notification.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/hooks/notification.py Wed Feb 17 13:45:34 2016 +0100 @@ -167,7 +167,7 @@ __abstract__ = True # do not register by default __select__ = NotificationHook.__select__ & hook.issued_from_user_query() events = ('before_update_entity',) - skip_attrs = set() + skip_attrs = set(['modification_date']) def __call__(self): cnx = self._cw diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/hooks/syncschema.py --- a/cubicweb/hooks/syncschema.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/hooks/syncschema.py Wed Feb 17 13:45:34 2016 +0100 @@ -606,21 +606,22 @@ # relations, but only if it's the last instance for this relation type # for other relations if (rschema.final or rschema.inlined): - rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, ' - 'R eid %%(r)s, X from_entity E, E eid %%(e)s' - % rdeftype, - {'r': rschema.eid, 'e': rdef.subject.eid}) - if rset[0][0] == 0 and not cnx.deleted_in_transaction(rdef.subject.eid): - ptypes = cnx.transaction_data.setdefault('pendingrtypes', set()) - ptypes.add(rschema.type) - DropColumn.get_instance(cnx).add_data((str(rdef.subject), str(rschema))) - elif rschema.inlined: - cnx.system_sql('UPDATE %s%s SET %s%s=NULL WHERE ' - 'EXISTS(SELECT 1 FROM entities ' - ' WHERE eid=%s%s AND type=%%(to_etype)s)' - % (SQL_PREFIX, rdef.subject, SQL_PREFIX, rdef.rtype, - SQL_PREFIX, rdef.rtype), - {'to_etype': rdef.object.type}) + if not cnx.deleted_in_transaction(rdef.subject.eid): + rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, ' + 'R eid %%(r)s, X from_entity E, E eid %%(e)s' + % rdeftype, + {'r': rschema.eid, 'e': rdef.subject.eid}) + if rset[0][0] == 0: + ptypes = cnx.transaction_data.setdefault('pendingrtypes', set()) + ptypes.add(rschema.type) + DropColumn.get_instance(cnx).add_data((str(rdef.subject), str(rschema))) + elif rschema.inlined: + cnx.system_sql('UPDATE %s%s SET %s%s=NULL WHERE ' + 'EXISTS(SELECT 1 FROM entities ' + ' WHERE eid=%s%s AND type=%%(to_etype)s)' + % (SQL_PREFIX, rdef.subject, SQL_PREFIX, rdef.rtype, + SQL_PREFIX, rdef.rtype), + {'to_etype': rdef.object.type}) elif lastrel: DropRelationTable(cnx, str(rschema)) else: diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/hooks/syncsession.py --- a/cubicweb/hooks/syncsession.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/hooks/syncsession.py Wed Feb 17 13:45:34 2016 +0100 @@ -148,7 +148,8 @@ """the observed connections set has been commited""" cwprop = self.cwprop if not cwprop.for_user: - self.cnx.vreg['propertyvalues'][cwprop.pkey] = cwprop.value + self.cnx.vreg['propertyvalues'][cwprop.pkey] = \ + self.cnx.vreg.typed_value(cwprop.pkey, cwprop.value) # if for_user is set, update is handled by a ChangeCWPropertyOp operation diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/hooks/test/data/hooks.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/hooks/test/data/hooks.py Wed Feb 17 13:45:34 2016 +0100 @@ -0,0 +1,8 @@ +from cubicweb.predicates import is_instance +from cubicweb.hooks import notification + + +class FolderUpdateHook(notification.EntityUpdateHook): + __select__ = (notification.EntityUpdateHook.__select__ & + is_instance('Folder')) + order = 100 # late trigger so that metadata hooks come before. diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/hooks/test/unittest_notification.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/hooks/test/unittest_notification.py Wed Feb 17 13:45:34 2016 +0100 @@ -0,0 +1,39 @@ +# copyright 2015 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 . +"""tests for notification hooks""" + +from cubicweb.devtools.testlib import CubicWebTC + + +class NotificationHooksTC(CubicWebTC): + + def test_entity_update(self): + """Check transaction_data['changes'] filled by "notifentityupdated" hook. + """ + with self.admin_access.repo_cnx() as cnx: + root = cnx.create_entity('Folder', name=u'a') + cnx.commit() + root.cw_set(name=u'b') + self.assertIn('changes', cnx.transaction_data) + self.assertEqual(cnx.transaction_data['changes'], + {root.eid: set([('name', u'a', u'b')])}) + + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/hooks/test/unittest_syncsession.py --- a/cubicweb/hooks/test/unittest_syncsession.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/hooks/test/unittest_syncsession.py Wed Feb 17 13:45:34 2016 +0100 @@ -71,6 +71,15 @@ req.execute('INSERT CWProperty X: X pkey "ui.language", X value "hop"') self.assertEqual(cm.exception.errors, {'value-subject': u'unauthorized value'}) + def test_vreg_propertyvalues_update(self): + self.vreg.register_property( + 'test.int', type='Int', help='', sitewide=True) + with self.admin_access.repo_cnx() as cnx: + cnx.execute('INSERT CWProperty X: X pkey "test.int", X value "42"') + cnx.commit() + self.assertEqual(self.vreg.property_value('test.int'), 42) + + if __name__ == '__main__': from logilab.common.testlib import unittest_main unittest_main() diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/misc/migration/3.21.0_Any.py --- a/cubicweb/misc/migration/3.21.0_Any.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/misc/migration/3.21.0_Any.py Wed Feb 17 13:45:34 2016 +0100 @@ -166,9 +166,9 @@ cstr, helper, prefix='cw_') args = {'e': rdef.subject.type, 'c': cstrname, 'v': check} if repo.system_source.dbdriver == 'postgres': - sql('ALTER TABLE cw_%(e)s DROP CONSTRAINT IF EXISTS %(c)s' % args) + sql('ALTER TABLE cw_%(e)s DROP CONSTRAINT IF EXISTS %(c)s' % args, ask_confirm=False) elif repo.system_source.dbdriver.startswith('sqlserver'): sql("IF OBJECT_ID('%(c)s', 'C') IS NOT NULL " - "ALTER TABLE cw_%(e)s DROP CONSTRAINT %(c)s" % args) - sql('ALTER TABLE cw_%(e)s ADD CONSTRAINT %(c)s CHECK(%(v)s)' % args) + "ALTER TABLE cw_%(e)s DROP CONSTRAINT %(c)s" % args, ask_confirm=False) + sql('ALTER TABLE cw_%(e)s ADD CONSTRAINT %(c)s CHECK(%(v)s)' % args, ask_confirm=False) commit() diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/misc/migration/3.22.0_Any.py --- a/cubicweb/misc/migration/3.22.0_Any.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/misc/migration/3.22.0_Any.py Wed Feb 17 13:45:34 2016 +0100 @@ -12,7 +12,7 @@ sql("SET TIME ZONE '%s'" % timezone) for entity in schema.entities(): - if entity.final: + if entity.final or entity.type not in fsschema: continue change_attribute_type(entity.type, 'creation_date', 'TZDatetime', ask_confirm=False) change_attribute_type(entity.type, 'modification_date', 'TZDatetime', ask_confirm=False) diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/misc/migration/3.22.1_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/misc/migration/3.22.1_Any.py Wed Feb 17 13:45:34 2016 +0100 @@ -0,0 +1,12 @@ +from os import unlink +from os.path import isfile, join +from cubicweb.cwconfig import CubicWebConfiguration as cwcfg + +regdir = cwcfg.instances_dir() + +if isfile(join(regdir, 'startorder')): + if confirm('The startorder file is not used anymore in Cubicweb 3.22. ' + 'Should I delete it?', + shell=False, pdb=False): + unlink(join(regdir, 'startorder')) + diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/misc/migration/postcreate.py --- a/cubicweb/misc/migration/postcreate.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/misc/migration/postcreate.py Wed Feb 17 13:45:34 2016 +0100 @@ -30,7 +30,7 @@ create_entity('CWProperty', pkey=u'system.version.%s' % cube.lower(), value=text_type(config.cube_version(cube))) -# some entities have been added before schema entities, fix the 'is' and +# some entities have been added before schema entities, add their missing 'is' and # 'is_instance_of' relations for rtype in ('is', 'is_instance_of'): sql('INSERT INTO %s_relation ' diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/predicates.py --- a/cubicweb/predicates.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/predicates.py Wed Feb 17 13:45:34 2016 +0100 @@ -498,7 +498,8 @@ except ValueError: page_size = None if page_size is None: - page_size = req.property_value('navigation.page-size') + page_size_prop = getattr(cls, 'page_size_property', 'navigation.page-size') + page_size = req.property_value(page_size_prop) if len(rset) <= (page_size*self.nbpages): return 0 return self.nbpages diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/server/__init__.py --- a/cubicweb/server/__init__.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/server/__init__.py Wed Feb 17 13:45:34 2016 +0100 @@ -297,6 +297,9 @@ # re-login using the admin user config._cubes = None # avoid assertion error repo = get_repository(config=config) + # replace previous schema by the new repo's one. This is necessary so that we give the proper + # schema to `initialize_schema` above since it will initialize .eid attribute of schema elements + schema = repo.schema with connect(repo, login, password=pwd) as cnx: with cnx.security_enabled(False, False): repo.system_source.eid = ssource.eid # redo this manually diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/server/repository.py --- a/cubicweb/server/repository.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/server/repository.py Wed Feb 17 13:45:34 2016 +0100 @@ -226,6 +226,11 @@ if not config.creating: self.info("set fs instance'schema") self.set_schema(config.load_schema(expand_cubes=True)) + if not config.creating: + # set eids on entities schema + with self.internal_cnx() as cnx: + for etype, eid in cnx.execute('Any XN,X WHERE X is CWEType, X name XN'): + self.schema.eschema(etype).eid = eid else: # normal start: load the instance schema from the database self.info('loading schema from the repository') diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/server/sources/datafeed.py --- a/cubicweb/server/sources/datafeed.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/server/sources/datafeed.py Wed Feb 17 13:45:34 2016 +0100 @@ -356,7 +356,7 @@ self.source.info('Using cwclientlib for %s' % url) resp = cnx.get(url) resp.raise_for_status() - return URLLibResponseAdapter(BytesIO(resp.text), url) + return URLLibResponseAdapter(BytesIO(resp.content), url) except (ImportError, ValueError, EnvironmentError) as exc: # ImportError: not available # ValueError: no config entry found diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/server/sources/native.py --- a/cubicweb/server/sources/native.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/server/sources/native.py Wed Feb 17 13:45:34 2016 +0100 @@ -43,7 +43,7 @@ from logilab.common.decorators import cached, clear_cache from logilab.common.configuration import Method -from logilab.common.shellutils import getlogin +from logilab.common.shellutils import getlogin, ASK from logilab.database import get_db_helper, sqlgen from yams.schema import role_name @@ -56,7 +56,7 @@ from cubicweb.cwconfig import CubicWebNoAppConfiguration from cubicweb.server import hook from cubicweb.server import schema2sql as y2sql -from cubicweb.server.utils import crypt_password, eschema_eid, verify_and_update +from cubicweb.server.utils import crypt_password, verify_and_update from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn from cubicweb.server.rqlannotation import set_qdata from cubicweb.server.hook import CleanupDeletedEidsCacheOp @@ -915,17 +915,14 @@ 'asource': text_type(source.uri)} self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs) # insert core relations: is, is_instance_of and cw_source - try: + + if entity.e_schema.eid is not None: # else schema has not yet been serialized self._handle_is_relation_sql(cnx, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)', - (entity.eid, eschema_eid(cnx, entity.e_schema))) - except IndexError: - # during schema serialization, skip - pass - else: + (entity.eid, entity.e_schema.eid)) for eschema in entity.e_schema.ancestors() + [entity.e_schema]: self._handle_is_relation_sql(cnx, 'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)', - (entity.eid, eschema_eid(cnx, eschema))) + (entity.eid, eschema.eid)) if 'CWSource' in self.schema and source.eid is not None: # else, cw < 3.10 self._handle_is_relation_sql(cnx, 'INSERT INTO cw_source_relation(eid_from,eid_to) VALUES (%s,%s)', (entity.eid, source.eid)) @@ -1742,15 +1739,20 @@ tables = archive.read('tables.txt').splitlines() sequences = archive.read('sequences.txt').splitlines() numranges = archive.read('numranges.txt').splitlines() - file_versions = self._parse_versions(archive.read('versions.txt')) - versions = set(self._get_versions()) - if file_versions != versions: - self.logger.critical('Unable to restore : versions do not match') - self.logger.critical('Expected:\n%s', '\n'.join('%s : %s' % (cube, ver) - for cube, ver in sorted(versions))) - self.logger.critical('Found:\n%s', '\n'.join('%s : %s' % (cube, ver) - for cube, ver in sorted(file_versions))) - raise ValueError('Unable to restore : versions do not match') + archive_versions = self._parse_versions(archive.read('versions.txt')) + db_versions = set(self._get_versions()) + if archive_versions != db_versions: + self.logger.critical('Restore warning: versions do not match') + new_cubes = db_versions - archive_versions + if new_cubes: + self.logger.critical('In the db:\n%s', '\n'.join('%s: %s' % (cube, ver) + for cube, ver in sorted(new_cubes))) + old_cubes = archive_versions - db_versions + if old_cubes: + self.logger.critical('In the archive:\n%s', '\n'.join('%s: %s' % (cube, ver) + for cube, ver in sorted(old_cubes))) + if not ASK.confirm('Versions mismatch: continue anyway ?', False): + raise ValueError('Unable to restore: versions do not match') table_chunks = {} for name in archive.namelist(): if not name.startswith('tables/'): diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/server/test/data-migractions/migratedapp/schema.py --- a/cubicweb/server/test/data-migractions/migratedapp/schema.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/server/test/data-migractions/migratedapp/schema.py Wed Feb 17 13:45:34 2016 +0100 @@ -211,3 +211,8 @@ class same_as(RelationDefinition): subject = ('Societe',) object = 'ExternalUri' + +class inlined_rel(RelationDefinition): + subject = object = 'Folder2' + inlined = True + cardinality = '??' diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/server/test/unittest_migractions.py --- a/cubicweb/server/test/unittest_migractions.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/server/test/unittest_migractions.py Wed Feb 17 13:45:34 2016 +0100 @@ -276,10 +276,10 @@ 'description', 'description_format', 'eid', 'filed_under2', 'has_text', - 'identity', 'in_basket', 'is', 'is_instance_of', + 'identity', 'in_basket', 'inlined_rel', 'is', 'is_instance_of', 'modification_date', 'name', 'owned_by']) self.assertCountEqual([str(rs) for rs in self.schema['Folder2'].object_relations()], - ['filed_under2', 'identity']) + ['filed_under2', 'identity', 'inlined_rel']) # Old will be missing as it has been renamed into 'New' in the migrated # schema while New hasn't been added here. self.assertEqual(sorted(str(e) for e in self.schema['filed_under2'].subjects()), diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/server/utils.py --- a/cubicweb/server/utils.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/server/utils.py Wed Feb 17 13:45:34 2016 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -31,6 +31,8 @@ from passlib.utils import handlers as uh, to_hash_str from passlib.context import CryptContext +from logilab.common.deprecation import deprecated + from cubicweb.md5crypt import crypt as md5crypt @@ -76,16 +78,14 @@ # wrong password return b'' - +@deprecated('[3.22] no more necessary, directly get eschema.eid') def eschema_eid(cnx, eschema): - """get eid of the CWEType entity for the given yams type. You should use - this because when schema has been loaded from the file-system, not from the - database, (e.g. during tests), eschema.eid is not set. + """get eid of the CWEType entity for the given yams type. + + This used to be necessary because when the schema has been loaded from the + file-system, not from the database, (e.g. during tests), eschema.eid was + not set. """ - if eschema.eid is None: - eschema.eid = cnx.execute( - 'Any X WHERE X is CWEType, X name %(name)s', - {'name': text_type(eschema)})[0][0] return eschema.eid diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/skeleton/DISTNAME.spec.tmpl --- a/cubicweb/skeleton/DISTNAME.spec.tmpl Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/skeleton/DISTNAME.spec.tmpl Wed Feb 17 13:45:34 2016 +0100 @@ -44,4 +44,4 @@ %%files %%defattr(-, root, root) -/* +%%{_prefix}/share/cubicweb/cubes/* diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/sobjects/test/unittest_notification.py --- a/cubicweb/sobjects/test/unittest_notification.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/sobjects/test/unittest_notification.py Wed Feb 17 13:45:34 2016 +0100 @@ -1,4 +1,3 @@ -# -*- coding: iso-8859-1 -*- # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # @@ -16,47 +15,10 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . +"""Tests for notification sobjects""" -from socket import gethostname - -from logilab.common.testlib import unittest_main, TestCase from cubicweb.devtools.testlib import CubicWebTC, MAILBOX -from cubicweb.mail import construct_message_id, parse_message_id - -class MessageIdTC(TestCase): - def test_base(self): - msgid1 = construct_message_id('testapp', 21) - msgid2 = construct_message_id('testapp', 21) - self.assertNotEqual(msgid1, msgid2) - self.assertNotIn('&', msgid1) - self.assertNotIn('=', msgid1) - self.assertNotIn('/', msgid1) - self.assertNotIn('+', msgid1) - values = parse_message_id(msgid1, 'testapp') - self.assertTrue(values) - # parse_message_id should work with or without surrounding <> - self.assertEqual(values, parse_message_id(msgid1[1:-1], 'testapp')) - self.assertEqual(values['eid'], '21') - self.assertIn('timestamp', values) - self.assertEqual(parse_message_id(msgid1[1:-1], 'anotherapp'), None) - - def test_notimestamp(self): - msgid1 = construct_message_id('testapp', 21, False) - msgid2 = construct_message_id('testapp', 21, False) - values = parse_message_id(msgid1, 'testapp') - self.assertEqual(values, {'eid': '21'}) - - def test_parse_message_doesnt_raise(self): - self.assertEqual(parse_message_id('oijioj@bla.bla', 'tesapp'), None) - self.assertEqual(parse_message_id('oijioj@bla', 'tesapp'), None) - self.assertEqual(parse_message_id('oijioj', 'tesapp'), None) - - - def test_nonregr_empty_message_id(self): - for eid in (1, 12, 123, 1234): - msgid1 = construct_message_id('testapp', eid, 12) - self.assertNotEqual(msgid1, '<@testapp.%s>' % gethostname()) class NotificationTC(CubicWebTC): @@ -67,7 +29,7 @@ 'WHERE U eid %(x)s', {'x': urset[0][0]}) req.execute('INSERT CWProperty X: X pkey "ui.language", X value "fr", X for_user U ' 'WHERE U eid %(x)s', {'x': urset[0][0]}) - req.cnx.commit() # commit so that admin get its properties updated + req.cnx.commit() # commit so that admin get its properties updated finder = self.vreg['components'].select('recipients_finder', req, rset=urset) self.set_option('default-recipients-mode', 'none') @@ -76,7 +38,8 @@ self.assertEqual(finder.recipients(), [(u'admin@logilab.fr', 'fr')]) self.set_option('default-recipients-mode', 'default-dest-addrs') self.set_option('default-dest-addrs', 'abcd@logilab.fr, efgh@logilab.fr') - self.assertEqual(list(finder.recipients()), [('abcd@logilab.fr', 'en'), ('efgh@logilab.fr', 'en')]) + self.assertEqual(list(finder.recipients()), + [('abcd@logilab.fr', 'en'), ('efgh@logilab.fr', 'en')]) def test_status_change_view(self): with self.admin_access.web_request() as req: @@ -99,5 +62,7 @@ self.assertEqual(email.subject, 'status changed CWUser #%s (admin)' % u.eid) + if __name__ == '__main__': + from logilab.common.testlib import unittest_main unittest_main() diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/test/unittest_mail.py --- a/cubicweb/test/unittest_mail.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/test/unittest_mail.py Wed Feb 17 13:45:34 2016 +0100 @@ -16,19 +16,18 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""unit tests for module cubicweb.mail - -""" +"""unit tests for module cubicweb.mail""" import os import re +from socket import gethostname import sys +from unittest import TestCase -from logilab.common.testlib import unittest_main from logilab.common.umessage import message_from_string from cubicweb.devtools.testlib import CubicWebTC -from cubicweb.mail import format_mail +from cubicweb.mail import format_mail, construct_message_id, parse_message_id def getlogin(): @@ -74,7 +73,6 @@ self.assertEqual(msg.get('reply-to'), u'oim , BimBam ') self.assertEqual(msg.get_payload(decode=True), u'un petit cöucou') - def test_format_mail_euro(self): mail = format_mail({'name': u'oîm', 'email': u'oim@logilab.fr'}, ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €') @@ -99,7 +97,6 @@ self.assertEqual(msg.get('reply-to'), u'oîm ') self.assertEqual(msg.get_payload(decode=True), u'un petit cöucou €') - def test_format_mail_from_reply_to(self): # no sender-name, sender-addr in the configuration self.set_option('sender-name', '') @@ -125,18 +122,20 @@ self.set_option('sender-addr', 'cubicweb-test@logilab.fr') # anonymous notification: no name and no email specified msg = format_mail({'name': u'', 'email': u''}, - ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €', - config=self.config) + ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €', + config=self.config) msg = message_from_string(msg.as_string()) self.assertEqual(msg.get('from'), u'cubicweb-test ') self.assertEqual(msg.get('reply-to'), u'cubicweb-test ') # anonymous notification: only email specified msg = format_mail({'email': u'tutu@logilab.fr'}, - ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €', - config=self.config) + ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €', + config=self.config) msg = message_from_string(msg.as_string()) self.assertEqual(msg.get('from'), u'cubicweb-test ') - self.assertEqual(msg.get('reply-to'), u'cubicweb-test , cubicweb-test ') + self.assertEqual( + msg.get('reply-to'), + u'cubicweb-test , cubicweb-test ') # anonymous notification: only name specified msg = format_mail({'name': u'tutu'}, ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €', @@ -146,6 +145,41 @@ self.assertEqual(msg.get('reply-to'), u'tutu ') +class MessageIdTC(TestCase): + + def test_base(self): + msgid1 = construct_message_id('testapp', 21) + msgid2 = construct_message_id('testapp', 21) + self.assertNotEqual(msgid1, msgid2) + self.assertNotIn('&', msgid1) + self.assertNotIn('=', msgid1) + self.assertNotIn('/', msgid1) + self.assertNotIn('+', msgid1) + values = parse_message_id(msgid1, 'testapp') + self.assertTrue(values) + # parse_message_id should work with or without surrounding <> + self.assertEqual(values, parse_message_id(msgid1[1:-1], 'testapp')) + self.assertEqual(values['eid'], '21') + self.assertIn('timestamp', values) + self.assertEqual(parse_message_id(msgid1[1:-1], 'anotherapp'), None) + + def test_notimestamp(self): + msgid1 = construct_message_id('testapp', 21, False) + construct_message_id('testapp', 21, False) + values = parse_message_id(msgid1, 'testapp') + self.assertEqual(values, {'eid': '21'}) + + def test_parse_message_doesnt_raise(self): + self.assertEqual(parse_message_id('oijioj@bla.bla', 'tesapp'), None) + self.assertEqual(parse_message_id('oijioj@bla', 'tesapp'), None) + self.assertEqual(parse_message_id('oijioj', 'tesapp'), None) + + def test_nonregr_empty_message_id(self): + for eid in (1, 12, 123, 1234): + msgid1 = construct_message_id('testapp', eid, 12) + self.assertNotEqual(msgid1, '<@testapp.%s>' % gethostname()) + if __name__ == '__main__': + from logilab.common.testlib import unittest_main unittest_main() diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/web/component.py --- a/cubicweb/web/component.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/web/component.py Wed Feb 17 13:45:34 2016 +0100 @@ -71,10 +71,13 @@ except AttributeError: page_size = self.cw_extra_kwargs.get('page_size') if page_size is None: - if 'page_size' in self._cw.form: - page_size = int(self._cw.form['page_size']) - else: - page_size = self._cw.property_value(self.page_size_property) + try: + page_size = int(self._cw.form.get('page_size')) + except (ValueError, TypeError): + # no or invalid value, fall back + pass + if page_size is None: + page_size = self._cw.property_value(self.page_size_property) self._page_size = page_size return page_size diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/web/data/cubicweb.goa.js --- a/cubicweb/web/data/cubicweb.goa.js Thu Feb 11 21:59:49 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,16 +0,0 @@ -/** - * functions specific to cubicweb on google appengine - * - * :organization: Logilab - * :copyright: 2008-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. - * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr - */ - -/** - * .. function:: rql_for_eid(eid) - * - * overrides rql_for_eid function from htmlhelpers.hs - */ -function rql_for_eid(eid) { - return 'Any X WHERE X eid "' + eid + '"'; -} diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/web/formfields.py --- a/cubicweb/web/formfields.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/web/formfields.py Wed Feb 17 13:45:34 2016 +0100 @@ -78,7 +78,7 @@ from yams.constraints import (SizeConstraint, StaticVocabularyConstraint, FormatConstraint) -from cubicweb import Binary, tags, uilib +from cubicweb import Binary, tags, uilib, neg_role from cubicweb.utils import support_args from cubicweb.web import INTERNAL_FIELD_VALUE, ProcessFormError, eid_param, \ formwidgets as fw @@ -1205,10 +1205,13 @@ else: targetschema = rdef.subject card = rdef.role_cardinality(role) + composite = getattr(rdef, 'composite', None) kwargs['name'] = rschema.type kwargs['role'] = role kwargs['eidparam'] = True - kwargs.setdefault('required', card in '1+') + # don't mark composite relation as required, we want the composite element + # to be removed when not linked to its parent + kwargs.setdefault('required', card in '1+' and composite != neg_role(role)) if role == 'object': kwargs.setdefault('label', (eschema.type, rschema.type + '_object')) else: diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/web/test/data/schema.py --- a/cubicweb/web/test/data/schema.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/web/test/data/schema.py Wed Feb 17 13:45:34 2016 +0100 @@ -95,6 +95,33 @@ class Ticket(EntityType): title = String(maxsize=32, required=True, fulltextindexed=True) concerns = SubjectRelation('Project', composite='object') + in_version = SubjectRelation('Version', composite='object', + cardinality='?*', inlined=True) + +class Version(EntityType): + name = String(required=True) + +class Filesystem(EntityType): + name = String() + +class DirectoryPermission(EntityType): + value = String() + +class parent_fs(RelationDefinition): + name = 'parent' + subject = 'Directory' + object = 'Filesystem' + +class Directory(EntityType): + name = String(required=True) + has_permission = SubjectRelation('DirectoryPermission', cardinality='*1', + composite='subject') + +class parent_directory(RelationDefinition): + name = 'parent' + subject = 'Directory' + object = 'Directory' + composite = 'object' class Folder(EntityType): name = String(required=True) diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/web/test/unittest_application.py --- a/cubicweb/web/test/unittest_application.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/web/test/unittest_application.py Wed Feb 17 13:45:34 2016 +0100 @@ -26,7 +26,6 @@ from logilab.common.testlib import TestCase, unittest_main from logilab.common.decorators import clear_cache, classproperty -from cubicweb import AuthenticationError from cubicweb import view from cubicweb.devtools.testlib import CubicWebTC, real_error_handling from cubicweb.devtools.fake import FakeRequest @@ -260,6 +259,259 @@ {'login-subject': u'the value "admin" is already used, use another one'}) self.assertEqual(forminfo['values'], req.form) + def _edit_parent(self, dir_eid, parent_eid, role='subject', + etype='Directory', **kwargs): + parent_eid = parent_eid or '__cubicweb_internal_field__' + with self.admin_access.web_request() as req: + req.form = { + 'eid': unicode(dir_eid), + '__maineid': unicode(dir_eid), + '__type:%s' % dir_eid: etype, + 'parent-%s:%s' % (role, dir_eid): parent_eid, + } + req.form.update(kwargs) + req.form['_cw_entity_fields:%s' % dir_eid] = ','.join( + ['parent-%s' % role] + + [key.split(':')[0] + for key in kwargs.keys() + if not key.startswith('_')]) + self.expect_redirect_handle_request(req) + + def _edit_in_version(self, ticket_eid, version_eid, **kwargs): + version_eid = version_eid or '__cubicweb_internal_field__' + with self.admin_access.web_request() as req: + req.form = { + 'eid': unicode(ticket_eid), + '__maineid': unicode(ticket_eid), + '__type:%s' % ticket_eid: 'Ticket', + 'in_version-subject:%s' % ticket_eid: version_eid, + } + req.form.update(kwargs) + req.form['_cw_entity_fields:%s' % ticket_eid] = ','.join( + ['in_version-subject'] + + [key.split(':')[0] + for key in kwargs.keys() + if not key.startswith('_')]) + self.expect_redirect_handle_request(req) + + def test_create_and_link_directories(self): + with self.admin_access.web_request() as req: + req.form = { + 'eid': (u'A', u'B'), + '__maineid': u'A', + '__type:A': 'Directory', + '__type:B': 'Directory', + 'parent-subject:B': u'A', + 'name-subject:A': u'topd', + 'name-subject:B': u'subd', + '_cw_entity_fields:A': 'name-subject', + '_cw_entity_fields:B': 'parent-subject,name-subject', + } + self.expect_redirect_handle_request(req) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', name=u'topd')) + self.assertTrue(cnx.find('Directory', name=u'subd')) + self.assertEqual(1, cnx.execute( + 'Directory SUBD WHERE SUBD parent TOPD,' + ' SUBD name "subd", TOPD name "topd"').rowcount) + + def test_create_subentity(self): + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + cnx.commit() + + with self.admin_access.web_request() as req: + req.form = { + 'eid': (unicode(topd.eid), u'B'), + '__maineid': unicode(topd.eid), + '__type:%s' % topd.eid: 'Directory', + '__type:B': 'Directory', + 'parent-object:%s' % topd.eid: u'B', + 'name-subject:B': u'subd', + '_cw_entity_fields:%s' % topd.eid: 'parent-object', + '_cw_entity_fields:B': 'name-subject', + } + self.expect_redirect_handle_request(req) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', name=u'topd')) + self.assertTrue(cnx.find('Directory', name=u'subd')) + self.assertEqual(1, cnx.execute( + 'Directory SUBD WHERE SUBD parent TOPD,' + ' SUBD name "subd", TOPD name "topd"').rowcount) + + def test_subject_subentity_removal(self): + """Editcontroller: detaching a composite relation removes the subentity + (edit from the subject side) + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + sub1 = cnx.create_entity('Directory', name=u'sub1', parent=topd) + sub2 = cnx.create_entity('Directory', name=u'sub2', parent=topd) + cnx.commit() + + attrs = {'name-subject:%s' % sub1.eid: ''} + self._edit_parent(sub1.eid, parent_eid=None, **attrs) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertFalse(cnx.find('Directory', eid=sub1.eid)) + self.assertTrue(cnx.find('Directory', eid=sub2.eid)) + + def test_object_subentity_removal(self): + """Editcontroller: detaching a composite relation removes the subentity + (edit from the object side) + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + sub1 = cnx.create_entity('Directory', name=u'sub1', parent=topd) + sub2 = cnx.create_entity('Directory', name=u'sub2', parent=topd) + cnx.commit() + + self._edit_parent(topd.eid, parent_eid=sub1.eid, role='object') + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertTrue(cnx.find('Directory', eid=sub1.eid)) + self.assertFalse(cnx.find('Directory', eid=sub2.eid)) + + def test_reparent_subentity(self): + "Editcontroller: re-parenting a subentity does not remove it" + with self.admin_access.repo_cnx() as cnx: + top1 = cnx.create_entity('Directory', name=u'top1') + top2 = cnx.create_entity('Directory', name=u'top2') + subd = cnx.create_entity('Directory', name=u'subd', parent=top1) + cnx.commit() + + self._edit_parent(subd.eid, parent_eid=top2.eid) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=top1.eid)) + self.assertTrue(cnx.find('Directory', eid=top2.eid)) + self.assertTrue(cnx.find('Directory', eid=subd.eid)) + self.assertEqual( + cnx.find('Directory', eid=subd.eid).one().parent[0], top2) + + def test_reparent_subentity_inlined(self): + """Editcontroller: re-parenting a subentity does not remove it + (inlined case)""" + with self.admin_access.repo_cnx() as cnx: + version1 = cnx.create_entity('Version', name=u'version1') + version2 = cnx.create_entity('Version', name=u'version2') + ticket = cnx.create_entity('Ticket', title=u'ticket', + in_version=version1) + cnx.commit() + + self._edit_in_version(ticket.eid, version_eid=version2.eid) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Version', eid=version1.eid)) + self.assertTrue(cnx.find('Version', eid=version2.eid)) + self.assertTrue(cnx.find('Ticket', eid=ticket.eid)) + self.assertEqual( + cnx.find('Ticket', eid=ticket.eid).one().in_version[0], version2) + + def test_subject_mixed_composite_subentity_removal_1(self): + """Editcontroller: detaching several subentities respects each rdef's + compositeness - Remove non composite + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + fs = cnx.create_entity('Filesystem', name=u'/tmp') + subd = cnx.create_entity('Directory', name=u'subd', + parent=(topd, fs)) + cnx.commit() + + self._edit_parent(subd.eid, parent_eid=topd.eid) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertTrue(cnx.find('Directory', eid=subd.eid)) + self.assertTrue(cnx.find('Filesystem', eid=fs.eid)) + self.assertEqual(cnx.find('Directory', eid=subd.eid).one().parent, + [topd,]) + + def test_subject_mixed_composite_subentity_removal_2(self): + """Editcontroller: detaching several subentities respects each rdef's + compositeness - Remove composite + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + fs = cnx.create_entity('Filesystem', name=u'/tmp') + subd = cnx.create_entity('Directory', name=u'subd', + parent=(topd, fs)) + cnx.commit() + + self._edit_parent(subd.eid, parent_eid=fs.eid) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertFalse(cnx.find('Directory', eid=subd.eid)) + self.assertTrue(cnx.find('Filesystem', eid=fs.eid)) + + def test_object_mixed_composite_subentity_removal_1(self): + """Editcontroller: detaching several subentities respects each rdef's + compositeness - Remove non composite + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + fs = cnx.create_entity('Filesystem', name=u'/tmp') + subd = cnx.create_entity('Directory', name=u'subd', + parent=(topd, fs)) + cnx.commit() + + self._edit_parent(fs.eid, parent_eid=None, role='object', + etype='Filesystem') + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertTrue(cnx.find('Directory', eid=subd.eid)) + self.assertTrue(cnx.find('Filesystem', eid=fs.eid)) + self.assertEqual(cnx.find('Directory', eid=subd.eid).one().parent, + [topd,]) + + def test_object_mixed_composite_subentity_removal_2(self): + """Editcontroller: detaching several subentities respects each rdef's + compositeness - Remove composite + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + fs = cnx.create_entity('Filesystem', name=u'/tmp') + subd = cnx.create_entity('Directory', name=u'subd', + parent=(topd, fs)) + cnx.commit() + + self._edit_parent(topd.eid, parent_eid=None, role='object') + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertFalse(cnx.find('Directory', eid=subd.eid)) + self.assertTrue(cnx.find('Filesystem', eid=fs.eid)) + + def test_delete_mandatory_composite(self): + with self.admin_access.repo_cnx() as cnx: + perm = cnx.create_entity('DirectoryPermission') + mydir = cnx.create_entity('Directory', name=u'dir', + has_permission=perm) + cnx.commit() + + with self.admin_access.web_request() as req: + dir_eid = unicode(mydir.eid) + perm_eid = unicode(perm.eid) + req.form = { + 'eid': [dir_eid, perm_eid], + '__maineid' : dir_eid, + '__type:%s' % dir_eid: 'Directory', + '__type:%s' % perm_eid: 'DirectoryPermission', + '_cw_entity_fields:%s' % dir_eid: '', + '_cw_entity_fields:%s' % perm_eid: 'has_permission-object', + 'has_permission-object:%s' % perm_eid: '', + } + path, _params = self.expect_redirect_handle_request(req, 'edit') + self.assertTrue(req.find('Directory', eid=mydir.eid)) + self.assertFalse(req.find('DirectoryPermission', eid=perm.eid)) + def test_ajax_view_raise_arbitrary_error(self): class ErrorAjaxView(view.View): __regid__ = 'test.ajax.error' diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/web/test/unittest_views_editforms.py --- a/cubicweb/web/test/unittest_views_editforms.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/web/test/unittest_views_editforms.py Wed Feb 17 13:45:34 2016 +0100 @@ -15,7 +15,9 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . + from logilab.common.testlib import unittest_main, mock_object +from logilab.common import tempattr from cubicweb.devtools.testlib import CubicWebTC from cubicweb.web.views import uicfg @@ -181,6 +183,22 @@ autoform = self.vreg['forms'].select('edition', req, entity=req.user) self.assertEqual(list(autoform.inlined_form_views()), []) + def test_inlined_form_views(self): + # when some relation has + cardinality, and some already linked entities which are not + # updatable, a link to optionally add a new sub-entity should be displayed, not a sub-form + # forcing creation of a sub-entity + from cubicweb.web.views import autoform + with self.admin_access.web_request() as req: + req.create_entity('EmailAddress', address=u'admin@cubicweb.org', + reverse_use_email=req.user.eid) + use_email_schema = self.vreg.schema['CWUser'].rdef('use_email') + with tempattr(use_email_schema, 'cardinality', '+1'): + with self.temporary_permissions(EmailAddress={'update': ()}): + form = self.vreg['forms'].select('edition', req, entity=req.user) + formviews = list(form.inlined_form_views()) + self.assertEqual(len(formviews), 1, formviews) + self.assertIsInstance(formviews[0], autoform.InlineAddNewLinkView) + def test_check_inlined_rdef_permissions(self): # try to check permissions when creating an entity ('user' below is a # fresh entity without an eid) diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/web/views/autoform.py --- a/cubicweb/web/views/autoform.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/web/views/autoform.py Wed Feb 17 13:45:34 2016 +0100 @@ -899,14 +899,13 @@ ttype = tschema.type formviews = list(self.inline_edition_form_view(rschema, ttype, role)) card = rschema.role_rdef(entity.e_schema, ttype, role).role_cardinality(role) - # there is no related entity and we need at least one: we need to - # display one explicit inline-creation view - if self.should_display_inline_creation_form(rschema, formviews, card): + related = entity.has_eid() and entity.related(rschema, role) + if self.should_display_inline_creation_form(rschema, related, card): formviews += self.inline_creation_form_view(rschema, ttype, role) # we can create more than one related entity, we thus display a link # to add new related entities if self.must_display_add_new_relation_link(rschema, role, tschema, - ttype, formviews, card): + ttype, related, card): addnewlink = self._cw.vreg['views'].select( 'inline-addnew-link', self._cw, etype=ttype, rtype=rschema, role=role, card=card, diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/web/views/debug.py diff -r 9b4de34ad394 -r 97095348b3ee cubicweb/web/views/editcontroller.py --- a/cubicweb/web/views/editcontroller.py Thu Feb 11 21:59:49 2016 +0100 +++ b/cubicweb/web/views/editcontroller.py Wed Feb 17 13:45:34 2016 +0100 @@ -31,7 +31,7 @@ from rql.utils import rqlvar_maker -from cubicweb import _, Binary, ValidationError +from cubicweb import _, Binary, ValidationError, UnknownEid from cubicweb.view import EntityAdapter from cubicweb.predicates import is_instance from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, @@ -79,12 +79,14 @@ self.edited = [] self.restrictions = [] self.kwargs = {} + self.canceled = False def __repr__(self): return ('Query ' % ( self.edited, self.restrictions, self.kwargs)) def insert_query(self, etype): + assert not self.canceled if self.edited: rql = 'INSERT %s X: %s' % (etype, ','.join(self.edited)) else: @@ -94,6 +96,7 @@ return rql def update_query(self, eid): + assert not self.canceled varmaker = rqlvar_maker() var = next(varmaker) while var in self.kwargs: @@ -192,6 +195,7 @@ # deserves special treatment req.data['pending_inlined'] = defaultdict(set) req.data['pending_others'] = set() + req.data['pending_composite_delete'] = set() try: for formparams in self._ordered_formparams(): eid = self.edit_entity(formparams) @@ -210,6 +214,9 @@ # then execute rql to set all relations for querydef in self.relations_rql: self._cw.execute(*querydef) + # delete pending composite + for entity in req.data['pending_composite_delete']: + entity.cw_delete() # XXX this processes *all* pending operations of *all* entities if '__delete' in req.form: todelete = req.list_form_param('__delete', req.form, pop=True) @@ -266,14 +273,17 @@ # creation, add relevant data to the rqlquery for form_, field in req.data['pending_inlined'].pop(entity.eid, ()): rqlquery.set_inlined(field.name, form_.edited_entity.eid) - if self.errors: - errors = dict((f.role_name(), text_type(ex)) for f, ex in self.errors) - raise ValidationError(valerror_eid(entity.eid), errors) - if eid is None: # creation or copy - entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery) - elif rqlquery.edited: # edition of an existant entity - self.check_concurrent_edition(formparams, eid) - self._update_entity(eid, rqlquery) + if not rqlquery.canceled: + if self.errors: + errors = dict((f.role_name(), text_type(ex)) for f, ex in self.errors) + raise ValidationError(valerror_eid(entity.eid), errors) + if eid is None: # creation or copy + entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery) + elif rqlquery.edited: # edition of an existant entity + self.check_concurrent_edition(formparams, eid) + self._update_entity(eid, rqlquery) + else: + self.errors = [] if is_main_entity: self.notify_edited(entity) if '__delete' in formparams: @@ -287,7 +297,8 @@ return eid def handle_formfield(self, form, field, rqlquery=None): - eschema = form.edited_entity.e_schema + entity = form.edited_entity + eschema = entity.e_schema try: for field, value in field.process_posted(form): if not ( @@ -295,25 +306,74 @@ or (field.role == 'object' and field.name in eschema.objrels)): continue + rschema = self._cw.vreg.schema.rschema(field.name) if rschema.final: rqlquery.set_attribute(field.name, value) + continue + + if entity.has_eid(): + origvalues = set(data[0] for data in entity.related(field.name, field.role).rows) else: - if form.edited_entity.has_eid(): - origvalues = set(entity.eid for entity in form.edited_entity.related(field.name, field.role, entities=True)) - else: - origvalues = set() - if value is None or value == origvalues: - continue # not edited / not modified / to do later - if rschema.inlined and rqlquery is not None and field.role == 'subject': - self.handle_inlined_relation(form, field, value, origvalues, rqlquery) - elif form.edited_entity.has_eid(): - self.handle_relation(form, field, value, origvalues) - else: - form._cw.data['pending_others'].add( (form, field) ) + origvalues = set() + if value is None or value == origvalues: + continue # not edited / not modified / to do later + + unlinked_eids = origvalues - value + + if unlinked_eids: + # Special handling of composite relation removal + self.handle_composite_removal( + form, field, unlinked_eids, value, rqlquery) + + if rschema.inlined and rqlquery is not None and field.role == 'subject': + self.handle_inlined_relation(form, field, value, origvalues, rqlquery) + elif form.edited_entity.has_eid(): + self.handle_relation(form, field, value, origvalues) + else: + form._cw.data['pending_others'].add( (form, field) ) + except ProcessFormError as exc: self.errors.append((field, exc)) + def handle_composite_removal(self, form, field, + removed_values, new_values, rqlquery): + """ + In EditController-handled forms, when the user removes a composite + relation, it triggers the removal of the related entity in the + composite. This is where this happens. + + See for instance test_subject_subentity_removal in + web/test/unittest_application.py. + """ + rschema = self._cw.vreg.schema.rschema(field.name) + new_value_etypes = set() + # the user could have included nonexisting eids in the POST; don't crash. + for eid in new_values: + try: + new_value_etypes.add(self._cw.entity_from_eid(eid).cw_etype) + except UnknownEid: + continue + for unlinked_eid in removed_values: + unlinked_entity = self._cw.entity_from_eid(unlinked_eid) + rdef = rschema.role_rdef(form.edited_entity.cw_etype, + unlinked_entity.cw_etype, + field.role) + if rdef.composite is not None: + if rdef.composite == field.role: + to_be_removed = unlinked_entity + else: + if unlinked_entity.cw_etype in new_value_etypes: + # This is a same-rdef re-parenting: do not remove the entity + continue + to_be_removed = form.edited_entity + self.info('Edition of %s is cancelled (deletion requested)', + to_be_removed) + rqlquery.canceled = True + self.info('Scheduling removal of %s as composite relation ' + '%s was removed', to_be_removed, rdef) + form._cw.data['pending_composite_delete'].add(to_be_removed) + def handle_inlined_relation(self, form, field, values, origvalues, rqlquery): """handle edition for the (rschema, x) relation of the given entity """ diff -r 9b4de34ad394 -r 97095348b3ee debian/changelog --- a/debian/changelog Thu Feb 11 21:59:49 2016 +0100 +++ b/debian/changelog Wed Feb 17 13:45:34 2016 +0100 @@ -1,9 +1,21 @@ +cubicweb (3.22.1-1) unstable; urgency=medium + + * new upstream release + + -- Julien Cristau Fri, 12 Feb 2016 10:38:56 +0100 + cubicweb (3.22.0-1) unstable; urgency=medium * new upstream release -- Julien Cristau Mon, 04 Jan 2016 17:53:55 +0100 +cubicweb (3.21.6-1) unstable; urgency=medium + + * new upstream release + + -- Julien Cristau Tue, 16 Feb 2016 18:53:15 +0100 + cubicweb (3.21.5-2) unstable; urgency=medium * Fix conflict between cubicweb-server and cubicweb-dev. @@ -46,6 +58,12 @@ -- Julien Cristau Fri, 10 Jul 2015 17:04:11 +0200 +cubicweb (3.20.13-1) unstable; urgency=medium + + * new upstream release + + -- Julien Cristau Tue, 16 Feb 2016 17:53:29 +0100 + cubicweb (3.20.12-1) unstable; urgency=medium * new upstream release. @@ -130,6 +148,12 @@ -- Julien Cristau Tue, 06 Jan 2015 18:11:03 +0100 +cubicweb (3.19.14-1) unstable; urgency=medium + + * new upstream release + + -- Julien Cristau Tue, 16 Feb 2016 11:01:47 +0100 + cubicweb (3.19.13-1) unstable; urgency=medium * New upstream release. diff -r 9b4de34ad394 -r 97095348b3ee debian/control --- a/debian/control Thu Feb 11 21:59:49 2016 +0100 +++ b/debian/control Wed Feb 17 13:45:34 2016 +0100 @@ -56,7 +56,8 @@ cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2, - python-passlib + python-passlib, + python-tz, Recommends: cubicweb-documentation (= ${source:Version}), Suggests: diff -r 9b4de34ad394 -r 97095348b3ee doc/book/devrepo/migration.rst --- a/doc/book/devrepo/migration.rst Thu Feb 11 21:59:49 2016 +0100 +++ b/doc/book/devrepo/migration.rst Wed Feb 17 13:45:34 2016 +0100 @@ -108,6 +108,7 @@ If some of the added cubes are already used by an instance, they'll simply be silently skipped. +To remove a cube use `drop_cube(cube, removedeps=False)`. Schema migration ---------------- diff -r 9b4de34ad394 -r 97095348b3ee doc/changes/3.22.rst --- a/doc/changes/3.22.rst Thu Feb 11 21:59:49 2016 +0100 +++ b/doc/changes/3.22.rst Wed Feb 17 13:45:34 2016 +0100 @@ -89,3 +89,6 @@ * the ``Repository.pinfo()`` method was removed * the ``cubicweb.utils.SizeConstrainedList`` class was removed + +* the 'startorder' file in configuration directory is no longer honored + diff -r 9b4de34ad394 -r 97095348b3ee doc/tools/pyjsrest.py --- a/doc/tools/pyjsrest.py Thu Feb 11 21:59:49 2016 +0100 +++ b/doc/tools/pyjsrest.py Wed Feb 17 13:45:34 2016 +0100 @@ -152,7 +152,6 @@ 'cubicweb.fckcwconfig.js', 'cubicweb.fckcwconfig-full.js', - 'cubicweb.goa.js', 'cubicweb.compat.js', ])