# HG changeset patch # User Rémi Cardona # Date 1436453036 -7200 # Node ID 1660a0fa4f433d8f37dd40b15762ea05c32bec8c # Parent d276e4b332ba9f1554c3001d06c9c89f1657b234# Parent 38b108b6bb1f4b4b0e7f4b10ac8c46b4cd2fd55a 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(). diff -r d276e4b332ba -r 1660a0fa4f43 .hgtags --- 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 diff -r d276e4b332ba -r 1660a0fa4f43 __pkginfo__.py --- 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" diff -r d276e4b332ba -r 1660a0fa4f43 cubicweb.spec --- 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 diff -r d276e4b332ba -r 1660a0fa4f43 debian/changelog --- 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 Thu, 09 Jul 2015 12:40:13 +0200 + cubicweb (3.20.8-1) unstable; urgency=low * New upstream release. diff -r d276e4b332ba -r 1660a0fa4f43 doc/book/annexes/rql/language.rst --- 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 diff -r d276e4b332ba -r 1660a0fa4f43 hooks/test/data-computed/schema.py --- 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 . -from yams.buildobjs import EntityType, String, Int, SubjectRelation +from yams.buildobjs import EntityType, String, Int, SubjectRelation, RelationDefinition THISYEAR = 2014 diff -r d276e4b332ba -r 1660a0fa4f43 server/sources/datafeed.py --- 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: diff -r d276e4b332ba -r 1660a0fa4f43 server/sources/rql2sql.py --- 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: diff -r d276e4b332ba -r 1660a0fa4f43 server/sources/storages.py --- 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 diff -r d276e4b332ba -r 1660a0fa4f43 server/test/unittest_postgres.py --- 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 diff -r d276e4b332ba -r 1660a0fa4f43 server/test/unittest_storage.py --- 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: diff -r d276e4b332ba -r 1660a0fa4f43 test/unittest_spa2rql.py --- 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 . +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) diff -r d276e4b332ba -r 1660a0fa4f43 web/formwidgets.py --- 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\')" % ( diff -r d276e4b332ba -r 1660a0fa4f43 web/test/unittest_views_forms.py --- 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 . +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() - diff -r d276e4b332ba -r 1660a0fa4f43 web/views/autoform.py --- 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 diff -r d276e4b332ba -r 1660a0fa4f43 web/views/staticcontrollers.py --- 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'