Merge with 3.22 branch
The merge was clean, just dropped cubicweb/web/data/cubicweb.goa.js.
--- 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
--- 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}
--- 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
--- 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"
--- 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
--- 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))
--- 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 <command>_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)
--- 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)
--- 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')
--- 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
--- 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)
--- 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)
--- 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.
"""
--- 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)
--- 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
--- 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
--- 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:
--- 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
--- /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.
--- /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 <http://www.gnu.org/licenses/>.
+"""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()
--- 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()
--- 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()
--- 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)
--- /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'))
+
--- 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 '
--- 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
--- 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
--- 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')
--- 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
--- 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/'):
--- 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 = '??'
--- 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()),
--- 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
--- 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/*
--- 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 <http://www.gnu.org/licenses/>.
+"""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()
--- 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 <http://www.gnu.org/licenses/>.
-"""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 <oim@logilab.fr>, BimBam <bim@boum.fr>')
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 <oim@logilab.fr>')
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 <cubicweb-test@logilab.fr>')
self.assertEqual(msg.get('reply-to'), u'cubicweb-test <cubicweb-test@logilab.fr>')
# 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 <tutu@logilab.fr>')
- self.assertEqual(msg.get('reply-to'), u'cubicweb-test <tutu@logilab.fr>, cubicweb-test <cubicweb-test@logilab.fr>')
+ self.assertEqual(
+ msg.get('reply-to'),
+ u'cubicweb-test <tutu@logilab.fr>, cubicweb-test <cubicweb-test@logilab.fr>')
# 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 <cubicweb-test@logilab.fr>')
+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()
--- 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
--- 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 + '"';
-}
--- 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:
--- 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)
--- 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'
--- 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 <http://www.gnu.org/licenses/>.
+
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)
--- 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,
--- 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 <edited=%r restrictions=%r kwargs=%r>' % (
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
"""
--- 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 <julien.cristau@logilab.fr> Fri, 12 Feb 2016 10:38:56 +0100
+
cubicweb (3.22.0-1) unstable; urgency=medium
* new upstream release
-- Julien Cristau <julien.cristau@logilab.fr> Mon, 04 Jan 2016 17:53:55 +0100
+cubicweb (3.21.6-1) unstable; urgency=medium
+
+ * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr> 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 <julien.cristau@logilab.fr> Fri, 10 Jul 2015 17:04:11 +0200
+cubicweb (3.20.13-1) unstable; urgency=medium
+
+ * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr> 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 <julien.cristau@logilab.fr> Tue, 06 Jan 2015 18:11:03 +0100
+cubicweb (3.19.14-1) unstable; urgency=medium
+
+ * new upstream release
+
+ -- Julien Cristau <julien.cristau@logilab.fr> Tue, 16 Feb 2016 11:01:47 +0100
+
cubicweb (3.19.13-1) unstable; urgency=medium
* New upstream release.
--- 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:
--- 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
----------------
--- 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
+
--- 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',
])