merge 3.20.9 into 3.21
authorRémi Cardona <remi.cardona@logilab.fr>
Thu, 09 Jul 2015 16:43:56 +0200
changeset 10522 1660a0fa4f43
parent 10518 d276e4b332ba (current diff)
parent 10521 38b108b6bb1f (diff)
child 10523 f245d865821e
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().
__pkginfo__.py
doc/book/annexes/rql/language.rst
server/sources/datafeed.py
server/sources/rql2sql.py
server/sources/storages.py
server/test/unittest_postgres.py
web/formwidgets.py
web/views/autoform.py
web/views/staticcontrollers.py
--- 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'