merge 3.20.9 into 3.21
Fix conflict in unittest_postgres.py by changing sources configuration
in PostgresTimeoutConfiguration.__init__ so it happens *after*
setUpModule().
--- a/.hgtags Wed Jul 08 09:37:06 2015 +0200
+++ b/.hgtags Thu Jul 09 16:43:56 2015 +0200
@@ -496,3 +496,6 @@
ec284980ed9e214fe6c15cc4cf9617961d88928d 3.20.8
ec284980ed9e214fe6c15cc4cf9617961d88928d debian/3.20.8-1
ec284980ed9e214fe6c15cc4cf9617961d88928d centos/3.20.8-1
+d477e64475821c21632878062bf68d142252ffc2 3.20.9
+d477e64475821c21632878062bf68d142252ffc2 debian/3.20.9-1
+d477e64475821c21632878062bf68d142252ffc2 centos/3.20.9-1
--- a/__pkginfo__.py Wed Jul 08 09:37:06 2015 +0200
+++ b/__pkginfo__.py Thu Jul 09 16:43:56 2015 +0200
@@ -22,7 +22,7 @@
modname = distname = "cubicweb"
-numversion = (3, 20, 8)
+numversion = (3, 20, 9)
version = '.'.join(str(num) for num in numversion)
description = "a repository of entities / relations for knowledge management"
--- a/cubicweb.spec Wed Jul 08 09:37:06 2015 +0200
+++ b/cubicweb.spec Thu Jul 09 16:43:56 2015 +0200
@@ -7,7 +7,7 @@
%endif
Name: cubicweb
-Version: 3.20.8
+Version: 3.20.9
Release: logilab.1%{?dist}
Summary: CubicWeb is a semantic web application framework
Source0: http://download.logilab.org/pub/cubicweb/cubicweb-%{version}.tar.gz
--- a/debian/changelog Wed Jul 08 09:37:06 2015 +0200
+++ b/debian/changelog Thu Jul 09 16:43:56 2015 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.20.9-1) unstable; urgency=low
+
+ * New upstream release.
+
+ -- RĂ©mi Cardona <remi.cardona@logilab.fr> Thu, 09 Jul 2015 12:40:13 +0200
+
cubicweb (3.20.8-1) unstable; urgency=low
* New upstream release.
--- a/doc/book/annexes/rql/language.rst Wed Jul 08 09:37:06 2015 +0200
+++ b/doc/book/annexes/rql/language.rst Thu Jul 09 16:43:56 2015 +0200
@@ -370,7 +370,7 @@
.. sourcecode:: sql
- DISTINCT ANY P WHERE V version_of P
+ DISTINCT Any P WHERE V version_of P
This will work, but is not efficient, as it will use the ``SELECT
DISTINCT`` SQL predicate, which needs to retrieve all projects, then
@@ -379,7 +379,7 @@
.. sourcecode:: sql
- ANY P WHERE EXISTS V version_of P
+ Any P WHERE EXISTS(V version_of P)
You can also use the question mark (`?`) to mark optional relations. This allows
--- a/hooks/test/data-computed/schema.py Wed Jul 08 09:37:06 2015 +0200
+++ b/hooks/test/data-computed/schema.py Thu Jul 09 16:43:56 2015 +0200
@@ -15,7 +15,7 @@
#
# 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 yams.buildobjs import EntityType, String, Int, SubjectRelation
+from yams.buildobjs import EntityType, String, Int, SubjectRelation, RelationDefinition
THISYEAR = 2014
--- a/server/sources/datafeed.py Wed Jul 08 09:37:06 2015 +0200
+++ b/server/sources/datafeed.py Thu Jul 09 16:43:56 2015 +0200
@@ -127,6 +127,9 @@
self.load_mapping(source_entity._cw)
def _get_parser(self, cnx, **kwargs):
+ if self.parser_id is None:
+ self.warning('No parser defined on source %r', self)
+ raise ObjectNotFound()
return self.repo.vreg['parsers'].select(
self.parser_id, cnx, source=self, **kwargs)
@@ -201,7 +204,10 @@
def _pull_data(self, cnx, force=False, raise_on_error=False):
importlog = self.init_import_log(cnx)
myuris = self.source_cwuris(cnx)
- parser = self._get_parser(cnx, sourceuris=myuris, import_log=importlog)
+ try:
+ parser = self._get_parser(cnx, sourceuris=myuris, import_log=importlog)
+ except ObjectNotFound:
+ return {}
if self.process_urls(parser, self.urls, raise_on_error):
self.warning("some error occurred, don't attempt to delete entities")
else:
--- a/server/sources/rql2sql.py Wed Jul 08 09:37:06 2015 +0200
+++ b/server/sources/rql2sql.py Thu Jul 09 16:43:56 2015 +0200
@@ -182,7 +182,7 @@
for sol in newsols:
invariants.setdefault(id(sol), {})[vname] = sol.pop(vname)
elif var.scope is not rqlst:
- # move appart variables which are in a EXISTS scope and are variating
+ # move apart variables which are in a EXISTS scope and are variating
try:
thisexistssols, thisexistsvars = existssols[var.scope]
except KeyError:
--- a/server/sources/storages.py Wed Jul 08 09:37:06 2015 +0200
+++ b/server/sources/storages.py Thu Jul 09 16:43:56 2015 +0200
@@ -21,6 +21,7 @@
import sys
from os import unlink, path as osp
from contextlib import contextmanager
+import tempfile
from yams.schema import role_name
@@ -84,20 +85,11 @@
# * handle backup/restore
def uniquify_path(dirpath, basename):
- """return a unique file name for `basename` in `dirpath`, or None
- if all attemps failed.
-
- XXX subject to race condition.
+ """return a file descriptor and unique file name for `basename` in `dirpath`
"""
- path = osp.join(dirpath, basename.replace(osp.sep, '-'))
- if not osp.isfile(path):
- return path
+ path = basename.replace(osp.sep, '-')
base, ext = osp.splitext(path)
- for i in xrange(1, 256):
- path = '%s%s%s' % (base, i, ext)
- if not osp.isfile(path):
- return path
- return None
+ return tempfile.mkstemp(prefix=base, suffix=ext, dir=dirpath)
@contextmanager
def fsimport(session):
@@ -122,16 +114,13 @@
# 0444 as in "only allow read bit in permission"
self._wmode = wmode
- def _writecontent(self, path, binary):
+ def _writecontent(self, fd, binary):
"""write the content of a binary in readonly file
- As the bfss never alter a create file it does not prevent it to work as
- intended. This is a beter safe than sorry approach.
+ As the bfss never alters an existing file it does not prevent it from
+ working as intended. This is a better safe than sorry approach.
"""
- write_flag = os.O_WRONLY | os.O_CREAT | os.O_EXCL
- if sys.platform == 'win32':
- write_flag |= os.O_BINARY
- fd = os.open(path, write_flag, self._wmode)
+ os.fchmod(fd, self._wmode)
fileobj = os.fdopen(fd, 'wb')
binary.to_file(fileobj)
fileobj.close()
@@ -154,10 +143,10 @@
binary = Binary.from_file(entity.cw_edited[attr].getvalue())
else:
binary = entity.cw_edited.pop(attr)
- fpath = self.new_fs_path(entity, attr)
+ fd, fpath = self.new_fs_path(entity, attr)
# bytes storage used to store file's path
entity.cw_edited.edited_attribute(attr, Binary(fpath))
- self._writecontent(fpath, binary)
+ self._writecontent(fd, binary)
AddFileOp.get_instance(entity._cw).add_data(fpath)
return binary
@@ -187,10 +176,9 @@
fpath = None
else:
# Get filename for it
- fpath = self.new_fs_path(entity, attr)
- assert not osp.exists(fpath)
+ fd, fpath = self.new_fs_path(entity, attr)
# write attribute value on disk
- self._writecontent(fpath, binary)
+ self._writecontent(fd, binary)
# Mark the new file as added during the transaction.
# The file will be removed on rollback
AddFileOp.get_instance(entity._cw).add_data(fpath)
@@ -222,13 +210,13 @@
name = entity.cw_attr_metadata(attr, 'name')
if name is not None:
basename.append(name.encode(self.fsencoding))
- fspath = uniquify_path(self.default_directory,
+ fd, fspath = uniquify_path(self.default_directory,
'_'.join(basename))
if fspath is None:
msg = entity._cw._('failed to uniquify path (%s, %s)') % (
self.default_directory, '_'.join(basename))
raise ValidationError(entity.eid, {role_name(attr, 'subject'): msg})
- return fspath
+ return fd, fspath
def current_fs_path(self, entity, attr):
"""return the current fs_path of the attribute, or None is the attr is
--- a/server/test/unittest_postgres.py Wed Jul 08 09:37:06 2015 +0200
+++ b/server/test/unittest_postgres.py Thu Jul 09 16:43:56 2015 +0200
@@ -38,8 +38,16 @@
stoppgcluster(__file__)
+class PostgresTimeoutConfiguration(PostgresApptestConfiguration):
+ def __init__(self, *args, **kwargs):
+ self.default_sources = PostgresApptestConfiguration.default_sources.copy()
+ self.default_sources['system'] = PostgresApptestConfiguration.default_sources['system'].copy()
+ self.default_sources['system']['db-statement-timeout'] = 200
+ super(PostgresTimeoutConfiguration, self).__init__(*args, **kwargs)
+
+
class PostgresFTITC(CubicWebTC):
- configcls = PostgresApptestConfiguration
+ configcls = PostgresTimeoutConfiguration
def test_eid_range(self):
# concurrent allocation of eid ranges
@@ -134,6 +142,12 @@
{'type-subject-value': u'"nogood"',
'type-subject-choices': u'"todo", "a", "b", "T", "lalala"'})
+ def test_statement_timeout(self):
+ with self.admin_access.repo_cnx() as cnx:
+ cnx.system_sql('select pg_sleep(0.1)')
+ with self.assertRaises(Exception):
+ cnx.system_sql('select pg_sleep(0.3)')
+
class PostgresLimitSizeTC(CubicWebTC):
configcls = PostgresApptestConfiguration
--- a/server/test/unittest_storage.py Wed Jul 08 09:37:06 2015 +0200
+++ b/server/test/unittest_storage.py Thu Jul 09 16:43:56 2015 +0200
@@ -20,6 +20,7 @@
from logilab.common.testlib import unittest_main, tag, Tags
from cubicweb.devtools.testlib import CubicWebTC
+from glob import glob
import os
import os.path as osp
import shutil
@@ -88,16 +89,21 @@
def test_bfss_storage(self):
with self.admin_access.repo_cnx() as cnx:
f1 = self.create_file(cnx)
- expected_filepath = osp.join(self.tempdir, '%s_data_%s' %
- (f1.eid, f1.data_name))
- self.assertTrue(osp.isfile(expected_filepath))
+ filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
+ self.assertEqual(len(filepaths), 1, filepaths)
+ expected_filepath = filepaths[0]
# file should be read only
self.assertFalse(os.access(expected_filepath, os.W_OK))
self.assertEqual(file(expected_filepath).read(), 'the-data')
cnx.rollback()
self.assertFalse(osp.isfile(expected_filepath))
+ filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
+ self.assertEqual(len(filepaths), 0, filepaths)
f1 = self.create_file(cnx)
cnx.commit()
+ filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
+ self.assertEqual(len(filepaths), 1, filepaths)
+ expected_filepath = filepaths[0]
self.assertEqual(file(expected_filepath).read(), 'the-data')
f1.cw_set(data=Binary('the new data'))
cnx.rollback()
@@ -114,7 +120,9 @@
with self.admin_access.repo_cnx() as cnx:
f1 = self.create_file(cnx)
expected_filepath = osp.join(self.tempdir, '%s_data_%s' % (f1.eid, f1.data_name))
- self.assertEqual(self.fspath(cnx, f1), expected_filepath)
+ base, ext = osp.splitext(expected_filepath)
+ self.assertTrue(self.fspath(cnx, f1).startswith(base))
+ self.assertTrue(self.fspath(cnx, f1).endswith(ext))
def test_bfss_fs_importing_doesnt_touch_path(self):
with self.admin_access.repo_cnx() as cnx:
--- a/test/unittest_spa2rql.py Wed Jul 08 09:37:06 2015 +0200
+++ b/test/unittest_spa2rql.py Thu Jul 09 16:43:56 2015 +0200
@@ -15,10 +15,17 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+import unittest
+
from logilab.common.testlib import TestCase, unittest_main
from cubicweb.devtools import TestServerConfiguration
from cubicweb.xy import xy
-from cubicweb.spa2rql import Sparql2rqlTranslator
+
+SKIPCAUSE = None
+try:
+ from cubicweb.spa2rql import Sparql2rqlTranslator
+except ImportError as exc:
+ SKIPCAUSE = str(exc)
xy.add_equivalence('Project', 'doap:Project')
xy.add_equivalence('Project creation_date', 'doap:Project doap:created')
@@ -31,6 +38,7 @@
schema = config.load_schema()
+@unittest.skipIf(SKIPCAUSE, SKIPCAUSE)
class XYTC(TestCase):
def setUp(self):
self.tr = Sparql2rqlTranslator(schema)
--- a/web/formwidgets.py Wed Jul 08 09:37:06 2015 +0200
+++ b/web/formwidgets.py Thu Jul 09 16:43:56 2015 +0200
@@ -1040,11 +1040,11 @@
self.value = ''
self.onclick = onclick
self.cwaction = cwaction
- self.attrs.setdefault('class', self.css_class)
def render(self, form, field=None, renderer=None):
label = form._cw._(self.label)
attrs = self.attrs.copy()
+ attrs.setdefault('class', self.css_class)
if self.cwaction:
assert self.onclick is None
attrs['onclick'] = "postForm('__action_%s', \'%s\', \'%s\')" % (
--- a/web/test/unittest_views_forms.py Wed Jul 08 09:37:06 2015 +0200
+++ b/web/test/unittest_views_forms.py Thu Jul 09 16:43:56 2015 +0200
@@ -16,7 +16,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/>.
+from logilab.common import tempattr, attrdict
+
from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.web.views.autoform import InlinedFormField
class InlinedFormTC(CubicWebTC):
@@ -39,8 +42,33 @@
petype='Salesterm')
self.assertEqual(formview.form.linked_to, {})
+ def test_remove_js_depending_on_cardinality(self):
+ with self.admin_access.web_request() as req:
+ formview = req.vreg['views'].select(
+ 'inline-creation', req,
+ etype='File', rtype='described_by_test', role='subject',
+ peid='A',
+ petype='Salesterm')
+ # cardinality is 1, can't remove
+ self.assertIsNone(formview._get_removejs())
+ rdef = self.schema['Salesterm'].rdef('described_by_test')
+ with tempattr(rdef, 'cardinality', '?*'):
+ self.assertTrue(formview._get_removejs())
+ with tempattr(rdef, 'cardinality', '+*'):
+ # formview has no parent info (pform). This is what happens
+ # when an inline form is requested through AJAX.
+ self.assertTrue(formview._get_removejs())
+ fakeview = attrdict(dict(rtype='described_by_test', role='subject'))
+ # formview is first, can't be removed
+ formview.pform = attrdict(fields=[InlinedFormField(view=formview),
+ InlinedFormField(view=fakeview)])
+ self.assertIsNone(formview._get_removejs())
+ # formview isn't first, can be removed
+ formview.pform = attrdict(fields=[InlinedFormField(view=fakeview),
+ InlinedFormField(view=formview)])
+ self.assertTrue(formview._get_removejs())
+
if __name__ == '__main__':
from logilab.common.testlib import unittest_main
unittest_main()
-
--- a/web/views/autoform.py Wed Jul 08 09:37:06 2015 +0200
+++ b/web/views/autoform.py Thu Jul 09 16:43:56 2015 +0200
@@ -214,6 +214,12 @@
return self.cw_rset.get_entity(self.cw_row, self.cw_col)
@property
+ def petype(self):
+ assert isinstance(self.peid, int)
+ pentity = self._cw.entity_from_eid(self.peid)
+ return pentity.e_schema.type
+
+ @property
@cached
def form(self):
entity = self._entity()
@@ -249,12 +255,25 @@
creation form.
"""
entity = self._entity()
- if isinstance(self.peid, int):
- pentity = self._cw.entity_from_eid(self.peid)
- petype = pentity.e_schema.type
- rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), petype)
- card= rdef.role_cardinality(self.role)
- if card == '1': # don't display remove link
+ rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), self.petype)
+ card = rdef.role_cardinality(self.role)
+ if card == '1': # don't display remove link
+ return None
+ # if cardinality is 1..n (+), dont display link to remove an inlined form for the first form
+ # allowing to edit the relation. To detect so:
+ #
+ # * if parent form (pform) is None, we're generated through an ajax call and so we know this
+ # is not the first form
+ #
+ # * if parent form is not None, look for previous InlinedFormField in the parent's form
+ # fields
+ if card == '+' and self.pform is not None:
+ # retrieve all field'views handling this relation and return None if we're the first of
+ # them
+ first_view = next(iter((f.view for f in self.pform.fields
+ if isinstance(f, InlinedFormField)
+ and f.view.rtype == self.rtype and f.view.role == self.role)))
+ if self == first_view:
return None
return self.removejs and self.removejs % (
self.peid, self.rtype, entity.eid)
@@ -314,7 +333,7 @@
def removejs(self):
entity = self._entity()
rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), self.petype)
- card= rdef.role_cardinality(self.role)
+ card = rdef.role_cardinality(self.role)
# when one is adding an inline entity for a relation of a single card,
# the 'add a new xxx' link disappears. If the user then cancel the addition,
# we have to make this link appears back. This is done by giving add new link
--- a/web/views/staticcontrollers.py Wed Jul 08 09:37:06 2015 +0200
+++ b/web/views/staticcontrollers.py Thu Jul 09 16:43:56 2015 +0200
@@ -178,9 +178,9 @@
class DataController(StaticFileController):
- """Controller in charge of serving static file in /data/
+ """Controller in charge of serving static files in /data/
- Handle modeconcat like url.
+ Handles mod_concat-like URLs.
"""
__regid__ = 'data'