# HG changeset patch # User Sylvain Thénault # Date 1505202570 -7200 # Node ID c615f945b38ad6411f4b1b8b43074e55d72f9684 # Parent cf5d11ac79fb43450cef450726907a1bda957942# Parent 457274b4e017a5b8d9860faf2757dc6c03120bf4 Merge with 3.25 branch diff -r 457274b4e017 -r c615f945b38a cubicweb/__pkginfo__.py --- a/cubicweb/__pkginfo__.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/__pkginfo__.py Tue Sep 12 09:49:30 2017 +0200 @@ -27,8 +27,8 @@ modname = distname = "cubicweb" -numversion = (3, 25, 2) -version = '.'.join(str(num) for num in numversion) +numversion = (3, 26, 0) +version = '.'.join(str(num) for num in numversion) + '.dev0' description = "a repository of entities / relations for knowledge management" author = "Logilab" diff -r 457274b4e017 -r c615f945b38a cubicweb/dataimport/importer.py --- a/cubicweb/dataimport/importer.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/dataimport/importer.py Tue Sep 12 09:49:30 2017 +0200 @@ -244,7 +244,7 @@ if not rschema.final: # .prepare() should drop other cases from the entity dict assert rschema.inlined - if not entity_dict[rtype] in extid2eid: + if entity_dict[rtype] not in extid2eid: return False # entity is ready, replace all relation's extid by eids for rtype in entity_dict: @@ -253,6 +253,21 @@ entity_dict[rtype] = extid2eid[entity_dict[rtype]] return True + def why_not_ready(self, extid2eid): + """Return some text explaining why this ext entity is not ready. + """ + assert self._schema, 'prepare() method should be called first on %s' % self + # as .prepare has been called, we know that .values only contains subject relation *type* as + # key (no more (rtype, role) tuple) + schema = self._schema + entity_dict = self.values + for rtype in entity_dict: + rschema = schema.rschema(rtype) + if not rschema.final: + if entity_dict[rtype] not in extid2eid: + return u'inlined relation %s is not present (%s)' % (rtype, entity_dict[rtype]) + raise AssertionError('this external entity seems actually ready for insertion') + class ExtEntitiesImporter(object): """This class is responsible for importing externals entities, that is instances of @@ -413,7 +428,8 @@ "missing data?"] for ext_entities in queue.values(): for ext_entity in ext_entities: - msgs.append(str(ext_entity)) + msg = '{}: {}'.format(ext_entity, ext_entity.why_not_ready(self.extid2eid)) + msgs.append(msg) map(error, msgs) if self.raise_on_error: raise Exception('\n'.join(msgs)) diff -r 457274b4e017 -r c615f945b38a cubicweb/dataimport/massive_store.py --- a/cubicweb/dataimport/massive_store.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/dataimport/massive_store.py Tue Sep 12 09:49:30 2017 +0200 @@ -186,6 +186,19 @@ def finish(self): """Remove temporary tables and columns.""" + try: + self._finish() + self._cnx.commit() + except Exception: + self._cnx.rollback() + raise + finally: + # delete the meta data table + self.sql('DROP TABLE IF EXISTS cwmassive_initialized') + self.commit() + + def _finish(self): + """Remove temporary tables and columns.""" assert not self.slave_mode, 'finish method should only be called by the master store' self.logger.info("Start cleaning") # Get all the initialized etypes/rtypes @@ -227,9 +240,6 @@ self._tmp_data_cleanup(tmp_tablename, rtype, uuid) # restore all deleted indexes and constraints self._dbh.restore_indexes_and_constraints() - # delete the meta data table - self.sql('DROP TABLE IF EXISTS cwmassive_initialized') - self.commit() def _insert_etype_metadata(self, etype, tmp_tablename): """Massive insertion of meta data for `etype`, with new entities in `tmp_tablename`. diff -r 457274b4e017 -r c615f945b38a cubicweb/dataimport/test/data-massimport/schema.py --- a/cubicweb/dataimport/test/data-massimport/schema.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/dataimport/test/data-massimport/schema.py Tue Sep 12 09:49:30 2017 +0200 @@ -48,7 +48,7 @@ Entity type for timezone of geonames. See timeZones.txt """ - code = String(maxsize=1024, indexed=True) + code = String(maxsize=1024, indexed=True, required=True) gmt = Float() dst = Float() raw_offset = Float() diff -r 457274b4e017 -r c615f945b38a cubicweb/dataimport/test/test_massive_store.py --- a/cubicweb/dataimport/test/test_massive_store.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/dataimport/test/test_massive_store.py Tue Sep 12 09:49:30 2017 +0200 @@ -16,6 +16,8 @@ # with this program. If not, see . """Massive store test case""" +import psycopg2 + from cubicweb.devtools import testlib, PostgresApptestConfiguration from cubicweb.devtools import startpgcluster, stoppgcluster from cubicweb.dataimport import ucsvreader, stores @@ -113,7 +115,7 @@ def test_massimport_etype_metadata(self): with self.admin_access.repo_cnx() as cnx: store = MassiveObjectStore(cnx) - timezone_eid = store.prepare_insert_entity('TimeZone') + timezone_eid = store.prepare_insert_entity('TimeZone', code=u'12') store.prepare_insert_entity('Location', timezone=timezone_eid) store.flush() store.commit() @@ -264,6 +266,16 @@ store.prepare_insert_entity('Location', name=u'toto') store.finish() + def test_delete_metatable_on_integrity_error(self): + with self.admin_access.repo_cnx() as cnx: + store = MassiveObjectStore(cnx) + store.prepare_insert_entity('TimeZone') + store.flush() + store.commit() + with self.assertRaises(psycopg2.IntegrityError): + store.finish() + self.assertNotIn('cwmassive_initialized', set(self.get_db_descr(cnx))) + if __name__ == '__main__': import unittest diff -r 457274b4e017 -r c615f945b38a cubicweb/dataimport/test/test_sqlgenstore.py --- a/cubicweb/dataimport/test/test_sqlgenstore.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/dataimport/test/test_sqlgenstore.py Tue Sep 12 09:49:30 2017 +0200 @@ -16,8 +16,6 @@ # with this program. If not, see . """SQL object store test case""" -import itertools - from cubicweb.dataimport import ucsvreader from cubicweb.devtools import testlib, PostgresApptestConfiguration from cubicweb.devtools import startpgcluster, stoppgcluster @@ -48,7 +46,7 @@ for code, gmt, dst, raw_offset in ucsvreader(open(self.datapath('timeZones.txt'), 'rb'), delimiter='\t'): cnx.create_entity('TimeZone', code=code, gmt=float(gmt), - dst=float(dst), raw_offset=float(raw_offset)) + dst=float(dst), raw_offset=float(raw_offset)) timezone_code = dict(cnx.execute('Any C, X WHERE X is TimeZone, X code C')) cnx.commit() # Push data @@ -70,12 +68,12 @@ 'alternatenames': infos[3], 'latitude': latitude, 'longitude': longitude, 'feature_class': feature_class, - 'alternate_country_code':infos[9], + 'alternate_country_code': infos[9], 'admin_code_3': infos[12], 'admin_code_4': infos[13], 'population': population, 'elevation': elevation, 'gtopo30': gtopo, 'timezone': timezone_code.get(infos[17]), - 'cwuri': u'http://sws.geonames.org/%s/' % int(infos[0]), + 'cwuri': u'http://sws.geonames.org/%s/' % int(infos[0]), 'geonameid': int(infos[0]), } store.prepare_insert_entity('Location', **entity) @@ -98,7 +96,7 @@ def test_sqlgenstore_etype_metadata(self): with self.admin_access.repo_cnx() as cnx: store = SQLGenObjectStore(cnx) - timezone_eid = store.prepare_insert_entity('TimeZone') + timezone_eid = store.prepare_insert_entity('TimeZone', code=u'12') store.prepare_insert_entity('Location', timezone=timezone_eid) store.flush() store.commit() @@ -120,5 +118,5 @@ if __name__ == '__main__': - from logilab.common.testlib import unittest_main - unittest_main() + import unittest + unittest.main() diff -r 457274b4e017 -r c615f945b38a cubicweb/req.py --- a/cubicweb/req.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/req.py Tue Sep 12 09:49:30 2017 +0200 @@ -213,26 +213,30 @@ >>> users = find('CWGroup', name=u"users").one() >>> groups = find('CWGroup').entities() """ - parts = ['Any X WHERE X is %s' % etype] + parts = ['Any X WHERE X is {0}'.format(etype)] varmaker = rqlvar_maker(defined='X') eschema = self.vreg.schema.eschema(etype) for attr, value in kwargs.items(): if isinstance(value, list) or isinstance(value, tuple): - raise NotImplementedError("List of values are not supported") + raise NotImplementedError( + '{0}: list of values are not supported'.format(attr)) if hasattr(value, 'eid'): kwargs[attr] = value.eid if attr.startswith('reverse_'): attr = attr[8:] - assert attr in eschema.objrels, \ - '%s not in %s object relations' % (attr, eschema) - parts.append( - '%(varname)s %(attr)s X, ' - '%(varname)s eid %%(reverse_%(attr)s)s' - % {'attr': attr, 'varname': next(varmaker)}) + if attr not in eschema.objrels: + raise KeyError('{0} not in {1} object relations'.format(attr, eschema)) + parts.append('{var} {attr} X, {var} eid %(reverse_{attr})s'.format( + var=next(varmaker), attr=attr)) else: - assert attr in eschema.subjrels, \ - '%s not in %s subject relations' % (attr, eschema) - parts.append('X %(attr)s %%(%(attr)s)s' % {'attr': attr}) + rel = eschema.subjrels.get(attr) + if rel is None: + raise KeyError('{0} not in {1} subject relations'.format(attr, eschema)) + if rel.final: + parts.append('X {attr} %({attr})s'.format(attr=attr)) + else: + parts.append('X {attr} {var}, {var} eid %({attr})s'.format( + attr=attr, var=next(varmaker))) rql = ', '.join(parts) diff -r 457274b4e017 -r c615f945b38a cubicweb/rqlrewrite.py --- a/cubicweb/rqlrewrite.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/rqlrewrite.py Tue Sep 12 09:49:30 2017 +0200 @@ -206,6 +206,19 @@ return stinfo['relations'] - stinfo['rhsrelations'] +def need_exists(node): + """Return true if the given node should be wrapped in an `Exists` node. + + This is true when node isn't already an `Exists` or `Not` node, nor a + `And`/`Or` of `Exists` or `Not` nodes. + """ + if isinstance(node, (n.Exists, n.Not)): + return False + if isinstance(node, (n.Or, n.And)): + return need_exists(node.children[0]) or need_exists(node.children[1]) + return True + + class Unsupported(Exception): """raised when an rql expression can't be inserted in some rql query because it create an unresolvable query (eg no solutions found) @@ -474,7 +487,7 @@ self.existingvars = existing def _inserted_root(self, new): - if not isinstance(new, (n.Exists, n.Not)): + if need_exists(new): new = n.Exists(new) return new @@ -649,6 +662,10 @@ # the snippet has introduced some ambiguities, we have to resolve them # "manually" variantes = self.build_variantes(newsolutions) + # if all ambiguities have been generated by variables within a "NOT + # EXISTS()#" or with type explicitly specified, we've nothing to change + if not variantes: + return newsolutions # insert "is" where necessary varexistsmap = {} self.removing_ambiguity = True @@ -680,21 +697,32 @@ variantes = set() for sol in newsolutions: variante = [] - for key, newvar in self.rewritten.items(): - variante.append((key, sol[newvar])) - variantes.add(tuple(variante)) - # rebuild variantes as dict - variantes = [dict(v) for v in variantes] - # remove variable which have always the same type - for key in self.rewritten: - it = iter(variantes) - etype = next(it)[key] - for variante in it: - if variante[key] != etype: - break - else: - for variante in variantes: - del variante[key] + for key, var_name in self.rewritten.items(): + var = self.select.defined_vars[var_name] + # skip variable which are only in a NOT EXISTS + if len(var.stinfo['relations']) == 1 and isinstance(var.scope.parent, n.Not): + continue + # skip variable whose type is already explicitly specified + if var.stinfo['typerel']: + continue + variante.append((key, sol[var_name])) + if variante: + variantes.add(tuple(variante)) + + if variantes: + # rebuild variantes as dict + variantes = [dict(v) for v in variantes] + # remove variable which have always the same type + for key in self.rewritten: + it = iter(variantes) + etype = next(it)[key] + for variante in it: + if variante[key] != etype: + break + else: + for variante in variantes: + del variante[key] + return variantes def _cleanup_inserted(self, node): diff -r 457274b4e017 -r c615f945b38a cubicweb/server/querier.py --- a/cubicweb/server/querier.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/server/querier.py Tue Sep 12 09:49:30 2017 +0200 @@ -293,7 +293,7 @@ if rqlexpr.check(cnx, eid): break else: - raise Unauthorized('No read acces on %r with eid %i.' % (var, eid)) + raise Unauthorized('No read access on %r with eid %i.' % (var, eid)) # mark variables protected by an rql expression restricted_vars.update(localcheck) # turn local check into a dict key diff -r 457274b4e017 -r c615f945b38a cubicweb/test/unittest_req.py --- a/cubicweb/test/unittest_req.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/test/unittest_req.py Tue Sep 12 09:49:30 2017 +0200 @@ -132,7 +132,10 @@ firstname=u'adrien', in_group=req.find('CWGroup', name=u'users').one()) - u = req.find('CWUser', login=u'cdevienne').one() + rset = req.find('CWUser', login=u'cdevienne') + self.assertEqual(rset.printable_rql(), + 'Any X WHERE X is CWUser, X login "cdevienne"') + u = rset.one() self.assertEqual(u.firstname, u"Christophe") users = list(req.find('CWUser').entities()) @@ -143,17 +146,26 @@ self.assertEqual(len(groups), 1) self.assertEqual(groups[0].name, u'users') - users = req.find('CWUser', in_group=groups[0]).entities() - users = list(users) + rset = req.find('CWUser', in_group=groups[0]) + self.assertEqual(rset.printable_rql(), + 'Any X WHERE X is CWUser, X in_group A, ' + 'A eid {0}'.format(groups[0].eid)) + users = list(rset.entities()) self.assertEqual(len(users), 2) - with self.assertRaises(AssertionError): + with self.assertRaisesRegexp( + KeyError, "^'chapeau not in CWUser subject relations'$" + ): req.find('CWUser', chapeau=u"melon") - with self.assertRaises(AssertionError): + with self.assertRaisesRegexp( + KeyError, "^'buddy not in CWUser object relations'$" + ): req.find('CWUser', reverse_buddy=users[0]) - with self.assertRaises(NotImplementedError): + with self.assertRaisesRegexp( + NotImplementedError, '^in_group: list of values are not supported$' + ): req.find('CWUser', in_group=[1, 2]) diff -r 457274b4e017 -r c615f945b38a cubicweb/test/unittest_rqlrewrite.py --- a/cubicweb/test/unittest_rqlrewrite.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/test/unittest_rqlrewrite.py Tue Sep 12 09:49:30 2017 +0200 @@ -18,15 +18,17 @@ from six import string_types -from logilab.common.testlib import unittest_main, TestCase from logilab.common.testlib import mock_object +from logilab.common.decorators import monkeypatch from yams import BadSchemaDefinition from yams.buildobjs import RelationDefinition from rql import parse, nodes, RQLHelper from cubicweb import Unauthorized, rqlrewrite, devtools +from cubicweb.rqlrewrite import RQLRewriter from cubicweb.schema import RRQLExpression, ERQLExpression from cubicweb.devtools import repotest +from cubicweb.devtools.testlib import CubicWebTC, TestCase def setUpModule(*args): @@ -40,46 +42,65 @@ 'has_text': 'fti'}) repotest.do_monkey_patch() + def tearDownModule(*args): repotest.undo_monkey_patch() global rqlhelper, schema del rqlhelper, schema + def eid_func_map(eid): return {1: 'CWUser', 2: 'Card', 3: 'Affaire'}[eid] + def _prepare_rewriter(rewriter_cls, kwargs): class FakeVReg: schema = schema + @staticmethod def solutions(sqlcursor, rqlst, kwargs): rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs) + class rqlhelper: @staticmethod def annotate(rqlst): rqlhelper.annotate(rqlst) - @staticmethod - def simplify(mainrqlst, needcopy=False): - rqlhelper.simplify(rqlst, needcopy) + return rewriter_cls(mock_object(vreg=FakeVReg, user=(mock_object(eid=1)))) + def rewrite(rqlst, snippets_map, kwargs, existingvars=None): rewriter = _prepare_rewriter(rqlrewrite.RQLRewriter, kwargs) + # turn {(V1, V2): constraints} into [(varmap, constraints)] snippets = [] + snippet_varmap = {} for v, exprs in sorted(snippets_map.items()): - rqlexprs = [isinstance(snippet, string_types) - and mock_object(snippet_rqlst=parse(u'Any X WHERE '+snippet).children[0], - expression=u'Any X WHERE '+snippet) - or snippet - for snippet in exprs] - snippets.append((dict([v]), rqlexprs)) + rqlexprs = [] + varmap = dict([v]) + for snippet in exprs: + # when the same snippet is impacting several variables, group them + # unless there is some conflicts on the snippet's variable name (we + # only want that for constraint on relations using both S and O) + if snippet in snippet_varmap and not ( + set(varmap.values()) & set(snippet_varmap[snippet].values())): + snippet_varmap[snippet].update(varmap) + continue + snippet_varmap[snippet] = varmap + if isinstance(snippet, string_types): + snippet = mock_object(snippet_rqlst=parse(u'Any X WHERE ' + snippet).children[0], + expression=u'Any X WHERE ' + snippet) + rqlexprs.append(snippet) + if rqlexprs: + snippets.append((varmap, rqlexprs)) + rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs) rewriter.rewrite(rqlst.children[0], snippets, kwargs, existingvars) check_vrefs(rqlst.children[0]) return rewriter.rewritten + def check_vrefs(node): vrefmaps = {} selects = [] @@ -88,14 +109,15 @@ try: vrefmaps[stmt].setdefault(vref.name, set()).add(vref) except KeyError: - vrefmaps[stmt] = {vref.name: set( (vref,) )} + vrefmaps[stmt] = {vref.name: set((vref,))} selects.append(stmt) assert node in selects, (node, selects) for stmt in selects: for var in stmt.defined_vars.values(): assert var.stinfo['references'] vrefmap = vrefmaps[stmt] - assert not (var.stinfo['references'] ^ vrefmap[var.name]), (node.as_string(), var, var.stinfo['references'], vrefmap[var.name]) + assert not (var.stinfo['references'] ^ vrefmap[var.name]), ( + node.as_string(), var, var.stinfo['references'], vrefmap[var.name]) class RQLRewriteTC(TestCase): @@ -109,72 +131,81 @@ def test_base_var(self): constraint = ('X in_state S, U in_group G, P require_state S,' - 'P name "read", P require_group G') + 'P name "read", P require_group G') rqlst = parse(u'Card C') rewrite(rqlst, {('C', 'X'): (constraint,)}, {}) - self.assertEqual(rqlst.as_string(), - u'Any C WHERE C is Card, B eid %(D)s, ' - 'EXISTS(C in_state A, B in_group E, F require_state A, ' - 'F name "read", F require_group E, A is State, E is CWGroup, F is CWPermission)') + self.assertEqual( + rqlst.as_string(), + u'Any C WHERE C is Card, B eid %(D)s, ' + 'EXISTS(C in_state A, B in_group E, F require_state A, ' + 'F name "read", F require_group E, A is State, E is CWGroup, F is CWPermission)') def test_multiple_var(self): card_constraint = ('X in_state S, U in_group G, P require_state S,' 'P name "read", P require_group G') affaire_constraints = ('X ref LIKE "PUBLIC%"', 'U in_group G, G name "public"') - kwargs = {'u':2} + kwargs = {'u': 2} rqlst = parse(u'Any S WHERE S documented_by C, C eid %(u)s') rewrite(rqlst, {('C', 'X'): (card_constraint,), ('S', 'X'): affaire_constraints}, kwargs) self.assertMultiLineEqual( rqlst.as_string(), u'Any S WHERE S documented_by C, C eid %(u)s, B eid %(D)s, ' - 'EXISTS(C in_state A, B in_group E, F require_state A, ' - 'F name "read", F require_group E, A is State, E is CWGroup, F is CWPermission), ' - '(EXISTS(S ref LIKE "PUBLIC%")) OR (EXISTS(B in_group G, G name "public", G is CWGroup)), ' - 'S is Affaire') + 'EXISTS(C in_state A, B in_group E, F require_state A, ' + 'F name "read", F require_group E, A is State, E is CWGroup, F is CWPermission), ' + '(EXISTS(S ref LIKE "PUBLIC%")) ' + 'OR (EXISTS(B in_group G, G name "public", G is CWGroup)), ' + 'S is Affaire') self.assertIn('D', kwargs) def test_or(self): - constraint = '(X identity U) OR (X in_state ST, CL identity U, CL in_state ST, ST name "subscribed")' + constraint = ( + '(X identity U) OR ' + '(X in_state ST, CL identity U, CL in_state ST, ST name "subscribed")' + ) rqlst = parse(u'Any S WHERE S owned_by C, C eid %(u)s, S is in (CWUser, CWGroup)') - rewrite(rqlst, {('C', 'X'): (constraint,)}, {'u':1}) - self.assertEqual(rqlst.as_string(), - 'Any S WHERE S owned_by C, C eid %(u)s, S is IN(CWUser, CWGroup), A eid %(B)s, ' - 'EXISTS((C identity A) OR (C in_state D, E identity A, ' - 'E in_state D, D name "subscribed"), D is State, E is CWUser)') + rewrite(rqlst, {('C', 'X'): (constraint,)}, {'u': 1}) + self.assertEqual( + rqlst.as_string(), + 'Any S WHERE S owned_by C, C eid %(u)s, S is IN(CWUser, CWGroup), A eid %(B)s, ' + 'EXISTS((C identity A) OR (C in_state D, E identity A, ' + 'E in_state D, D name "subscribed"), D is State, E is CWUser)') def test_simplified_rqlst(self): constraint = ('X in_state S, U in_group G, P require_state S,' - 'P name "read", P require_group G') - rqlst = parse(u'Any 2') # this is the simplified rql st for Any X WHERE X eid 12 + 'P name "read", P require_group G') + rqlst = parse(u'Any 2') # this is the simplified rql st for Any X WHERE X eid 12 rewrite(rqlst, {('2', 'X'): (constraint,)}, {}) - self.assertEqual(rqlst.as_string(), - u'Any 2 WHERE B eid %(C)s, ' - 'EXISTS(2 in_state A, B in_group D, E require_state A, ' - 'E name "read", E require_group D, A is State, D is CWGroup, E is CWPermission)') + self.assertEqual( + rqlst.as_string(), + u'Any 2 WHERE B eid %(C)s, ' + 'EXISTS(2 in_state A, B in_group D, E require_state A, ' + 'E name "read", E require_group D, A is State, D is CWGroup, E is CWPermission)') def test_optional_var_1(self): constraint = ('X in_state S, U in_group G, P require_state S,' - 'P name "read", P require_group G') + 'P name "read", P require_group G') rqlst = parse(u'Any A,C WHERE A documented_by C?') rewrite(rqlst, {('C', 'X'): (constraint,)}, {}) - self.assertEqual(rqlst.as_string(), - u'Any A,C WHERE A documented_by C?, A is Affaire ' - 'WITH C BEING ' - '(Any C WHERE EXISTS(C in_state B, D in_group F, G require_state B, G name "read", ' - 'G require_group F), D eid %(A)s, C is Card)') + self.assertEqual( + rqlst.as_string(), + u'Any A,C WHERE A documented_by C?, A is Affaire ' + 'WITH C BEING ' + '(Any C WHERE EXISTS(C in_state B, D in_group F, G require_state B, G name "read", ' + 'G require_group F), D eid %(A)s, C is Card)') def test_optional_var_2(self): constraint = ('X in_state S, U in_group G, P require_state S,' - 'P name "read", P require_group G') + 'P name "read", P require_group G') rqlst = parse(u'Any A,C,T WHERE A documented_by C?, C title T') rewrite(rqlst, {('C', 'X'): (constraint,)}, {}) - self.assertEqual(rqlst.as_string(), - u'Any A,C,T WHERE A documented_by C?, A is Affaire ' - 'WITH C,T BEING ' - '(Any C,T WHERE C title T, EXISTS(C in_state B, D in_group F, ' - 'G require_state B, G name "read", G require_group F), ' - 'D eid %(A)s, C is Card)') + self.assertEqual( + rqlst.as_string(), + u'Any A,C,T WHERE A documented_by C?, A is Affaire ' + 'WITH C,T BEING ' + '(Any C,T WHERE C title T, EXISTS(C in_state B, D in_group F, ' + 'G require_state B, G name "read", G require_group F), ' + 'D eid %(A)s, C is Card)') def test_optional_var_3(self): constraint1 = ('X in_state S, U in_group G, P require_state S,' @@ -182,12 +213,14 @@ constraint2 = 'X in_state S, S name "public"' rqlst = parse(u'Any A,C,T WHERE A documented_by C?, C title T') rewrite(rqlst, {('C', 'X'): (constraint1, constraint2)}, {}) - self.assertEqual(rqlst.as_string(), - u'Any A,C,T WHERE A documented_by C?, A is Affaire ' - 'WITH C,T BEING (Any C,T WHERE C title T, ' - '(EXISTS(C in_state B, D in_group F, G require_state B, G name "read", G require_group F)) ' - 'OR (EXISTS(C in_state E, E name "public")), ' - 'D eid %(A)s, C is Card)') + self.assertEqual( + rqlst.as_string(), + u'Any A,C,T WHERE A documented_by C?, A is Affaire ' + 'WITH C,T BEING (Any C,T WHERE C title T, ' + '(EXISTS(C in_state B, D in_group F, G require_state B, ' + 'G name "read", G require_group F)) ' + 'OR (EXISTS(C in_state E, E name "public")), ' + 'D eid %(A)s, C is Card)') def test_optional_var_4(self): constraint1 = 'A created_by U, X documented_by A' @@ -197,29 +230,38 @@ rewrite(rqlst, {('LA', 'X'): (constraint1, constraint2), ('X', 'X'): (constraint3,), ('Y', 'X'): (constraint3,)}, {}) - self.assertEqual(rqlst.as_string(), - u'Any X,LA,Y WHERE LA? documented_by X, LA concerne Y, B eid %(C)s, ' - 'EXISTS(X created_by B), EXISTS(Y created_by B), ' - 'X is Card, Y is IN(Division, Note, Societe) ' - 'WITH LA BEING (Any LA WHERE (EXISTS(A created_by B, LA documented_by A)) OR (EXISTS(E created_by B, LA concerne E)), ' - 'B eid %(D)s, LA is Affaire)') - + self.assertEqual( + rqlst.as_string(), + u'Any X,LA,Y WHERE LA? documented_by X, LA concerne Y, B eid %(C)s, ' + 'EXISTS(X created_by B), EXISTS(Y created_by B), ' + 'X is Card, Y is IN(Division, Note, Societe) ' + 'WITH LA BEING (Any LA WHERE (EXISTS(A created_by B, LA documented_by A)) ' + 'OR (EXISTS(E created_by B, LA concerne E)), ' + 'B eid %(D)s, LA is Affaire)') def test_ambiguous_optional_same_exprs(self): """See #3013535""" # see test of the same name in RewriteFullTC: original problem is # unreproducible here because it actually lies in # RQLRewriter.insert_local_checks - rqlst = parse(u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, A eid %(a)s, X creation_date CD') - rewrite(rqlst, {('X', 'X'): ('X created_by U',),}, {'a': 3}) - self.assertEqual(rqlst.as_string(), - u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, A eid %(a)s WITH X,CD BEING (Any X,CD WHERE X creation_date CD, EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))') + rqlst = parse(u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, ' + 'A eid %(a)s, X creation_date CD') + rewrite(rqlst, {('X', 'X'): ('X created_by U',)}, {'a': 3}) + self.assertEqual( + rqlst.as_string(), + u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, A eid %(a)s ' + 'WITH X,CD BEING (Any X,CD WHERE X creation_date CD, ' + 'EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))') def test_ambiguous_optional_same_exprs_constant(self): - rqlst = parse(u'Any A,AR,X WHERE A concerne X?, A ref AR, A eid %(a)s, X creation_date TODAY') - rewrite(rqlst, {('X', 'X'): ('X created_by U',),}, {'a': 3}) - self.assertEqual(rqlst.as_string(), - u'Any A,AR,X WHERE A concerne X?, A ref AR, A eid %(a)s WITH X BEING (Any X WHERE X creation_date TODAY, EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))') + rqlst = parse(u'Any A,AR,X WHERE A concerne X?, A ref AR, ' + 'A eid %(a)s, X creation_date TODAY') + rewrite(rqlst, {('X', 'X'): ('X created_by U',)}, {'a': 3}) + self.assertEqual( + rqlst.as_string(), + u'Any A,AR,X WHERE A concerne X?, A ref AR, A eid %(a)s ' + 'WITH X BEING (Any X WHERE X creation_date TODAY, ' + 'EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))') def test_optional_var_inlined(self): c1 = ('X require_permission P') @@ -229,12 +271,13 @@ ('A', 'X'): (c2,), }, {}) # XXX suboptimal - self.assertEqual(rqlst.as_string(), - "Any C,A,R WITH A,C,R BEING " - "(Any A,C,R WHERE A? inlined_card C, A ref R, " - "(A is NULL) OR (EXISTS(A inlined_card B, B require_permission D, " - "B is Card, D is CWPermission)), " - "A is Affaire, C is Card, EXISTS(C require_permission E, E is CWPermission))") + self.assertEqual( + rqlst.as_string(), + "Any C,A,R WITH A,C,R BEING " + "(Any A,C,R WHERE A? inlined_card C, A ref R, " + "(A is NULL) OR (EXISTS(A inlined_card B, B require_permission D, " + "B is Card, D is CWPermission)), " + "A is Affaire, C is Card, EXISTS(C require_permission E, E is CWPermission))") # def test_optional_var_inlined_has_perm(self): # c1 = ('X require_permission P') @@ -249,7 +292,8 @@ def test_optional_var_inlined_imbricated_error(self): c1 = ('X require_permission P') c2 = ('X inlined_card O, O require_permission P') - rqlst = parse(u'Any C,A,R,A2,R2 WHERE A? inlined_card C, A ref R,A2? inlined_card C, A2 ref R2') + rqlst = parse(u'Any C,A,R,A2,R2 WHERE A? inlined_card C, A ref R,' + 'A2? inlined_card C, A2 ref R2') self.assertRaises(BadSchemaDefinition, rewrite, rqlst, {('C', 'X'): (c1,), ('A', 'X'): (c2,), @@ -258,7 +302,6 @@ def test_optional_var_inlined_linked(self): c1 = ('X require_permission P') - c2 = ('X inlined_card O, O require_permission P') rqlst = parse(u'Any A,W WHERE A inlined_card C?, C inlined_note N, ' 'N inlined_affaire W') rewrite(rqlst, {('C', 'X'): (c1,)}, {}) @@ -295,6 +338,7 @@ self.assertEqual(rqlst.as_string(), 'Any C WHERE C in_state STATE?, C is Card, ' 'EXISTS(STATE name "hop"), STATE is State') + def test_relation_optimization_2_rhs(self): snippet = ('TW? subworkflow_exit X, TW name "hop"') rqlst = parse(u'SubWorkflowExitPoint EXIT WHERE C? subworkflow_exit EXIT') @@ -362,10 +406,11 @@ def test_unsupported_constraint_2(self): trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"') rqlst = parse(u'Any U,T WHERE U is CWUser, T wf_info_for U') - rewrite(rqlst, {('T', 'X'): (trinfo_constraint, 'X wf_info_for Y, Y in_group G, G name "managers"')}, {}) + rewrite(rqlst, {('T', 'X'): (trinfo_constraint, + 'X wf_info_for Y, Y in_group G, G name "managers"')}, {}) self.assertEqual(rqlst.as_string(), u'Any U,T WHERE U is CWUser, T wf_info_for U, ' - 'EXISTS(U in_group B, B name "managers", B is CWGroup), T is TrInfo') + u'EXISTS(U in_group B, B name "managers", B is CWGroup), T is TrInfo') def test_unsupported_constraint_3(self): self.skipTest('raise unauthorized for now') @@ -379,17 +424,20 @@ constraint = ('X concerne Y') rqlst = parse(u'Affaire X') rewrite(rqlst, {('X', 'X'): (constraint,)}, {}) - self.assertEqual(rqlst.as_string(), - u"Any X WHERE X is Affaire, ((EXISTS(X concerne A, A is Division)) OR (EXISTS(X concerne C, C is Societe))) OR (EXISTS(X concerne B, B is Note))") + self.assertEqual( + rqlst.as_string(), + u"Any X WHERE X is Affaire, ((EXISTS(X concerne A, A is Division)) " + "OR (EXISTS(X concerne C, C is Societe))) OR (EXISTS(X concerne B, B is Note))") def test_add_ambiguity_outerjoin(self): constraint = ('X concerne Y') rqlst = parse(u'Any X,C WHERE X? documented_by C') rewrite(rqlst, {('X', 'X'): (constraint,)}, {}) # ambiguity are kept in the sub-query, no need to be resolved using OR - self.assertEqual(rqlst.as_string(), - u"Any X,C WHERE X? documented_by C, C is Card WITH X BEING (Any X WHERE EXISTS(X concerne A), X is Affaire)") - + self.assertEqual( + rqlst.as_string(), + u"Any X,C WHERE X? documented_by C, C is Card " + "WITH X BEING (Any X WHERE EXISTS(X concerne A), X is Affaire)") def test_rrqlexpr_nonexistant_subject_1(self): constraint = RRQLExpression('S owned_by U') @@ -418,26 +466,33 @@ 'Any C WHERE C is Card, B eid %(D)s, EXISTS(A owned_by B, A is Card)') rqlst = parse(u'Card C') rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SOU') - self.assertEqual(rqlst.as_string(), - 'Any C WHERE C is Card, A eid %(B)s, EXISTS(C owned_by A, D owned_by A, D is Card)') + self.assertEqual( + rqlst.as_string(), + 'Any C WHERE C is Card, A eid %(B)s, EXISTS(C owned_by A, D owned_by A, D is Card)') def test_rrqlexpr_nonexistant_subject_3(self): constraint = RRQLExpression('U in_group G, G name "users"') rqlst = parse(u'Card C') rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SU') - self.assertEqual(rqlst.as_string(), - u'Any C WHERE C is Card, A eid %(B)s, EXISTS(A in_group D, D name "users", D is CWGroup)') + self.assertEqual( + rqlst.as_string(), + u'Any C WHERE C is Card, A eid %(B)s, ' + 'EXISTS(A in_group D, D name "users", D is CWGroup)') def test_rrqlexpr_nonexistant_subject_4(self): constraint = RRQLExpression('U in_group G, G name "users", S owned_by U') rqlst = parse(u'Card C') rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SU') - self.assertEqual(rqlst.as_string(), - u'Any C WHERE C is Card, A eid %(B)s, EXISTS(A in_group D, D name "users", C owned_by A, D is CWGroup)') + self.assertEqual( + rqlst.as_string(), + u'Any C WHERE C is Card, A eid %(B)s, ' + 'EXISTS(A in_group D, D name "users", C owned_by A, D is CWGroup)') rqlst = parse(u'Card C') rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'OU') - self.assertEqual(rqlst.as_string(), - u'Any C WHERE C is Card, A eid %(B)s, EXISTS(A in_group D, D name "users", D is CWGroup)') + self.assertEqual( + rqlst.as_string(), + u'Any C WHERE C is Card, A eid %(B)s, ' + 'EXISTS(A in_group D, D name "users", D is CWGroup)') def test_rrqlexpr_nonexistant_subject_5(self): constraint = RRQLExpression('S owned_by Z, O owned_by Z, O is Card') @@ -450,22 +505,28 @@ constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X') rqlst = parse(u'Affaire A WHERE NOT EXISTS(A documented_by C)') rewrite(rqlst, {('C', 'X'): (constraint,)}, {}, 'X') - self.assertEqual(rqlst.as_string(), - u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire') + self.assertEqual( + rqlst.as_string(), + u'Any A WHERE NOT EXISTS(A documented_by C, ' + 'EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire') def test_rqlexpr_not_relation_1_2(self): constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X') rqlst = parse(u'Affaire A WHERE NOT EXISTS(A documented_by C)') rewrite(rqlst, {('A', 'X'): (constraint,)}, {}, 'X') - self.assertEqual(rqlst.as_string(), - u'Any A WHERE NOT EXISTS(A documented_by C, C is Card), A is Affaire, EXISTS(A owned_by B, B login "hop", B is CWUser)') + self.assertEqual( + rqlst.as_string(), + u'Any A WHERE NOT EXISTS(A documented_by C, C is Card), A is Affaire, ' + 'EXISTS(A owned_by B, B login "hop", B is CWUser)') def test_rqlexpr_not_relation_2(self): constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X') rqlst = rqlhelper.parse(u'Affaire A WHERE NOT A documented_by C', annotate=False) rewrite(rqlst, {('C', 'X'): (constraint,)}, {}, 'X') - self.assertEqual(rqlst.as_string(), - u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire') + self.assertEqual( + rqlst.as_string(), + u'Any A WHERE NOT EXISTS(A documented_by C, ' + 'EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire') def test_rqlexpr_multiexpr_outerjoin(self): c1 = ERQLExpression('X owned_by Z, Z login "hop"', 'X') @@ -473,11 +534,12 @@ c3 = ERQLExpression('X owned_by Z, Z login "momo"', 'X') rqlst = rqlhelper.parse(u'Any A WHERE A documented_by C?', annotate=False) rewrite(rqlst, {('C', 'X'): (c1, c2, c3)}, {}, 'X') - self.assertEqual(rqlst.as_string(), - u'Any A WHERE A documented_by C?, A is Affaire ' - 'WITH C BEING (Any C WHERE ((EXISTS(C owned_by B, B login "hop")) ' - 'OR (EXISTS(C owned_by D, D login "momo"))) ' - 'OR (EXISTS(C owned_by A, A login "hip")), C is Card)') + self.assertEqual( + rqlst.as_string(), + u'Any A WHERE A documented_by C?, A is Affaire ' + 'WITH C BEING (Any C WHERE ((EXISTS(C owned_by B, B login "hop")) ' + 'OR (EXISTS(C owned_by D, D login "momo"))) ' + 'OR (EXISTS(C owned_by A, A login "hip")), C is Card)') def test_multiple_erql_one_bad(self): #: reproduce bug #2236985 @@ -503,7 +565,36 @@ 'Any O WHERE S use_email O, S is CWUser, O is EmailAddress, ' 'EXISTS(NOT S in_group A, A name "guests", A is CWGroup)') -from cubicweb.devtools.testlib import CubicWebTC + def test_ambiguous_constraint_not_exists(self): + state_constraint = ( + 'NOT EXISTS(A require_permission S) ' + 'OR EXISTS(B require_permission S, B is Card, O name "state1")' + 'OR EXISTS(C require_permission S, C is Note, O name "state2")' + ) + rqlst = parse(u'Any P WHERE NOT P require_state S') + rewrite(rqlst, {('P', 'S'): (state_constraint,), ('S', 'O'): (state_constraint,)}, {}) + self.assertMultiLineEqual( + rqlst.as_string(), + u'Any P WHERE NOT P require_state S, ' + '((NOT EXISTS(A require_permission P, A is IN(Card, Note)))' + ' OR (EXISTS(B require_permission P, B is Card, S name "state1")))' + ' OR (EXISTS(C require_permission P, C is Note, S name "state2")), ' + 'P is CWPermission, S is State') + + def test_ambiguous_using_is_in_function(self): + state_constraint = ( + 'NOT EXISTS(A require_permission S) ' + 'OR EXISTS(B require_permission S, B is IN (Card, Note), O name "state1")' + ) + rqlst = parse(u'Any P WHERE NOT P require_state S') + rewrite(rqlst, {('P', 'S'): (state_constraint,), ('S', 'O'): (state_constraint,)}, {}) + self.assertMultiLineEqual( + rqlst.as_string(), + u'Any P WHERE NOT P require_state S, ' + '(NOT EXISTS(A require_permission P, A is IN(Card, Note))) ' + 'OR (EXISTS(B require_permission P, B is IN(Card, Note), S name "state1")), ' + 'P is CWPermission, S is State') + class RewriteFullTC(CubicWebTC): appid = 'data-rewrite' @@ -512,7 +603,7 @@ if args is None: args = {} querier = self.repo.querier - union = parse(rql) # self.vreg.parse(rql, annotate=True) + union = parse(rql) # self.vreg.parse(rql, annotate=True) with self.admin_access.repo_cnx() as cnx: self.vreg.solutions(cnx, union, args) querier._annotate(union) @@ -546,7 +637,6 @@ union = self.process('Any A,AR,X,CD WHERE A concerne X?, A ref AR, X creation_date CD') self.assertEqual(union.as_string(), 'not generated today') - def test_xxxx(self): edef1 = self.schema['Societe'] edef2 = self.schema['Division'] @@ -564,12 +654,11 @@ def test_question_mark_attribute_snippet(self): # see #3661918 - from cubicweb.rqlrewrite import RQLRewriter - from logilab.common.decorators import monkeypatch repotest.undo_monkey_patch() orig_insert_snippets = RQLRewriter.insert_snippets # patch insert_snippets and not rewrite, insert_snippets is already # monkey patches (see above setupModule/repotest) + @monkeypatch(RQLRewriter) def insert_snippets(self, snippets, varexistsmap=None): # crash occurs if snippets are processed in a specific order, force @@ -632,21 +721,21 @@ 'C role D, D name "illustrator")', rqlst.as_string()) - def test_rewrite2(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'Any A,B WHERE A illustrator_of B, C require_permission R, S' 'require_state O') rule_rewrite(rqlst, rules) - self.assertEqual('Any A,B WHERE C require_permission R, S require_state O, ' - 'D is Contribution, D contributor A, D manifestation B, D role E, ' - 'E name "illustrator"', - rqlst.as_string()) + self.assertEqual( + 'Any A,B WHERE C require_permission R, S require_state O, ' + 'D is Contribution, D contributor A, D manifestation B, D role E, ' + 'E name "illustrator"', + rqlst.as_string()) def test_rewrite3(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'Any A,B WHERE E require_permission T, A illustrator_of B') rule_rewrite(rqlst, rules) self.assertEqual('Any A,B WHERE E require_permission T, ' @@ -656,7 +745,7 @@ def test_rewrite4(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'Any A,B WHERE C require_permission R, A illustrator_of B') rule_rewrite(rqlst, rules) self.assertEqual('Any A,B WHERE C require_permission R, ' @@ -666,7 +755,7 @@ def test_rewrite5(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'Any A,B WHERE C require_permission R, A illustrator_of B, ' 'S require_state O') rule_rewrite(rqlst, rules) @@ -678,7 +767,7 @@ # Tests for the with clause def test_rewrite_with(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'Any A,B WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)') rule_rewrite(rqlst, rules) self.assertEqual('Any A,B WITH A,B BEING ' @@ -688,8 +777,9 @@ def test_rewrite_with2(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} - rqlst = rqlhelper.parse(u'Any A,B WHERE T require_permission C WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)') + 'C manifestation O, C role R, R name "illustrator"'} + rqlst = rqlhelper.parse(u'Any A,B WHERE T require_permission C ' + 'WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)') rule_rewrite(rqlst, rules) self.assertEqual('Any A,B WHERE T require_permission C ' 'WITH A,B BEING (Any X,Y WHERE A is Contribution, ' @@ -707,32 +797,34 @@ def test_rewrite_with4(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'Any A,B WHERE A illustrator_of B ' - 'WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)') + 'WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)') rule_rewrite(rqlst, rules) - self.assertEqual('Any A,B WHERE C is Contribution, ' - 'C contributor A, C manifestation B, C role D, ' - 'D name "illustrator" WITH A,B BEING ' - '(Any X,Y WHERE A is Contribution, A contributor X, ' - 'A manifestation Y, A role B, B name "illustrator")', - rqlst.as_string()) + self.assertEqual( + 'Any A,B WHERE C is Contribution, ' + 'C contributor A, C manifestation B, C role D, ' + 'D name "illustrator" WITH A,B BEING ' + '(Any X,Y WHERE A is Contribution, A contributor X, ' + 'A manifestation Y, A role B, B name "illustrator")', + rqlst.as_string()) # Tests for the union def test_rewrite_union(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'(Any A,B WHERE A illustrator_of B) UNION' '(Any X,Y WHERE X is CWUser, Z manifestation Y)') rule_rewrite(rqlst, rules) - self.assertEqual('(Any A,B WHERE C is Contribution, ' - 'C contributor A, C manifestation B, C role D, ' - 'D name "illustrator") UNION (Any X,Y WHERE X is CWUser, Z manifestation Y)', - rqlst.as_string()) + self.assertEqual( + '(Any A,B WHERE C is Contribution, ' + 'C contributor A, C manifestation B, C role D, ' + 'D name "illustrator") UNION (Any X,Y WHERE X is CWUser, Z manifestation Y)', + rqlst.as_string()) def test_rewrite_union2(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'(Any Y WHERE Y match W) UNION ' '(Any A WHERE A illustrator_of B) UNION ' '(Any Y WHERE Y is ArtWork)') @@ -746,9 +838,9 @@ # Tests for the exists clause def test_rewrite_exists(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'(Any A,B WHERE A illustrator_of B, ' - 'EXISTS(B is ArtWork))') + 'EXISTS(B is ArtWork))') rule_rewrite(rqlst, rules) self.assertEqual('Any A,B WHERE EXISTS(B is ArtWork), ' 'C is Contribution, C contributor A, C manifestation B, C role D, ' @@ -757,7 +849,7 @@ def test_rewrite_exists2(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'(Any A,B WHERE B contributor A, EXISTS(A illustrator_of W))') rule_rewrite(rqlst, rules) self.assertEqual('Any A,B WHERE B contributor A, ' @@ -767,7 +859,7 @@ def test_rewrite_exists3(self): rules = {'illustrator_of': 'C is Contribution, C contributor S, ' - 'C manifestation O, C role R, R name "illustrator"'} + 'C manifestation O, C role R, R name "illustrator"'} rqlst = rqlhelper.parse(u'(Any A,B WHERE A illustrator_of B, EXISTS(A illustrator_of W))') rule_rewrite(rqlst, rules) self.assertEqual('Any A,B WHERE EXISTS(C is Contribution, C contributor A, ' @@ -779,13 +871,14 @@ # Test for GROUPBY def test_rewrite_groupby(self): rules = {'participated_in': 'S contributor O'} - rqlst = rqlhelper.parse(u'Any SUM(SA) GROUPBY S WHERE P participated_in S, P manifestation SA') + rqlst = rqlhelper.parse(u'Any SUM(SA) GROUPBY S ' + 'WHERE P participated_in S, P manifestation SA') rule_rewrite(rqlst, rules) self.assertEqual('Any SUM(SA) GROUPBY S WHERE P manifestation SA, P contributor S', rqlst.as_string()) -class RQLRelationRewriterTC(CubicWebTC): +class RQLRelationRewriterCWTC(CubicWebTC): appid = 'data-rewrite' @@ -794,8 +887,8 @@ art = cnx.create_entity('ArtWork', name=u'Les travailleurs de la Mer') role = cnx.create_entity('Role', name=u'illustrator') vic = cnx.create_entity('Person', name=u'Victor Hugo') - contrib = cnx.create_entity('Contribution', code=96, contributor=vic, - manifestation=art, role=role) + cnx.create_entity('Contribution', code=96, contributor=vic, + manifestation=art, role=role) rset = cnx.execute('Any X WHERE X illustrator_of S') self.assertEqual([u'Victor Hugo'], [result.name for result in rset.entities()]) @@ -816,4 +909,5 @@ if __name__ == '__main__': - unittest_main() + import unittest + unittest.main() diff -r 457274b4e017 -r c615f945b38a cubicweb/web/component.py --- a/cubicweb/web/component.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/web/component.py Tue Sep 12 09:49:30 2017 +0200 @@ -126,7 +126,7 @@ return url def ajax_page_url(self, **params): - divid = params.setdefault('divid', 'pageContent') + divid = params.setdefault('divid', 'contentmain') params['rql'] = self.cw_rset.printable_rql() return js_href("$(%s).loadxhtml(AJAX_PREFIX_URL, %s, 'get', 'swap')" % ( json_dumps('#'+divid), js.ajaxFuncArgs('view', params))) diff -r 457274b4e017 -r c615f945b38a cubicweb/web/test/unittest_views_basecontrollers.py --- a/cubicweb/web/test/unittest_views_basecontrollers.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/web/test/unittest_views_basecontrollers.py Tue Sep 12 09:49:30 2017 +0200 @@ -1012,6 +1012,13 @@ f = appobject(req) self.assertEqual(f(12, 13), '25') + def test_badrequest(self): + with self.assertRaises(RemoteCallFailed) as cm: + with self.remote_calling('foo'): + pass + self.assertEqual(cm.exception.status, 400) + self.assertEqual(cm.exception.reason, 'no foo method') + class JSonControllerTC(AjaxControllerTC): # NOTE: this class performs the same tests as AjaxController but with diff -r 457274b4e017 -r c615f945b38a cubicweb/web/views/ajaxcontroller.py --- a/cubicweb/web/views/ajaxcontroller.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/web/views/ajaxcontroller.py Tue Sep 12 09:49:30 2017 +0200 @@ -67,6 +67,7 @@ from functools import partial from six import PY2, text_type +from six.moves import http_client from logilab.common.date import strptime from logilab.common.registry import yes @@ -116,7 +117,8 @@ try: fname = self._cw.form['fname'] except KeyError: - raise RemoteCallFailed('no method specified') + raise RemoteCallFailed('no method specified', + status=http_client.BAD_REQUEST) # 1/ check first for old-style (JSonController) ajax func for bw compat try: func = getattr(basecontrollers.JSonController, 'js_%s' % fname) @@ -128,11 +130,13 @@ try: func = self._cw.vreg['ajax-func'].select(fname, self._cw) except ObjectNotFound: - raise RemoteCallFailed('no %s method' % fname) + raise RemoteCallFailed('no %s method' % fname, + status=http_client.BAD_REQUEST) else: warn('[3.15] remote function %s found on JSonController, ' 'use AjaxFunction / @ajaxfunc instead' % fname, DeprecationWarning, stacklevel=2) + debug_mode = self._cw.vreg.config.debugmode # no attribute means the callback takes no argument args = self._cw.form.get('arg', ()) if not isinstance(args, (list, tuple)): @@ -140,16 +144,20 @@ try: args = [json.loads(arg) for arg in args] except ValueError as exc: - self.exception('error while decoding json arguments for ' - 'js_%s: %s (err: %s)', fname, args, exc) - raise RemoteCallFailed(exc_message(exc, self._cw.encoding)) + if debug_mode: + self.exception('error while decoding json arguments for ' + 'js_%s: %s (err: %s)', fname, args, exc) + raise RemoteCallFailed(exc_message(exc, self._cw.encoding), + status=http_client.BAD_REQUEST) try: result = func(*args) except (RemoteCallFailed, DirectResponse): raise except Exception as exc: - self.exception('an exception occurred while calling js_%s(%s): %s', - fname, args, exc) + if debug_mode: + self.exception( + 'an exception occurred while calling js_%s(%s): %s', + fname, args, exc) raise RemoteCallFailed(exc_message(exc, self._cw.encoding)) if result is None: return '' @@ -219,7 +227,8 @@ try: return self._cw.execute(rql, args) except Exception as ex: - self.exception("error in _exec(rql=%s): %s", rql, ex) + if self._cw.vreg.config.debugmode: + self.exception("error in _exec(rql=%s): %s", rql, ex) return None return None @@ -232,29 +241,25 @@ stream = UStringIO() kwargs['w'] = stream.write assert not paginate - if divid == 'pageContent': + if divid == 'contentmain': # ensure divid isn't reused by the view (e.g. table view) del self._cw.form['divid'] - # mimick main template behaviour - stream.write(u'
') - vtitle = self._cw.form.get('vtitle') - if vtitle: - stream.write(u'

%s

\n' % vtitle) paginate = True + if divid == 'contentmain': + stream.write(u'
') nav_html = UStringIO() if paginate and not view.handle_pagination: view.paginate(w=nav_html.write) stream.write(nav_html.getvalue()) - if divid == 'pageContent': - stream.write(u'
') view.render(**kwargs) + stream.write(nav_html.getvalue()) + if divid == 'contentmain': + stream.write(u'
') extresources = self._cw.html_headers.getvalue(skiphead=True) if extresources: stream.write(u'
\n') # XXX use a widget? stream.write(extresources) stream.write(u'
\n') - if divid == 'pageContent': - stream.write(u'
%s
' % nav_html.getvalue()) return stream.getvalue() diff -r 457274b4e017 -r c615f945b38a cubicweb/web/views/basetemplates.py --- a/cubicweb/web/views/basetemplates.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/web/views/basetemplates.py Tue Sep 12 09:49:30 2017 +0200 @@ -158,14 +158,14 @@ 'etypenavigation', self._cw, rset=self.cw_rset) if etypefilter and etypefilter.cw_propval('visible'): etypefilter.render(w=w) + w(u'
\n') nav_html = UStringIO() if view and not view.handle_pagination: view.paginate(w=nav_html.write) w(nav_html.getvalue()) - w(u'
\n') view.render(w=w) + w(nav_html.getvalue()) w(u'
\n') # close id=contentmain - w(nav_html.getvalue()) w(u'
\n') # closes id=pageContent self.template_footer(view) diff -r 457274b4e017 -r c615f945b38a cubicweb/web/views/facets.py --- a/cubicweb/web/views/facets.py Tue Sep 12 09:48:02 2017 +0200 +++ b/cubicweb/web/views/facets.py Tue Sep 12 09:49:30 2017 +0200 @@ -246,7 +246,7 @@ rset, vid, divid, paginate = context else: rset = self.cw_rset - vid, divid = None, 'pageContent' + vid, divid = None, 'contentmain' paginate = view and view.paginable return rset, vid, divid, paginate diff -r 457274b4e017 -r c615f945b38a flake8-ok-files.txt --- a/flake8-ok-files.txt Tue Sep 12 09:48:02 2017 +0200 +++ b/flake8-ok-files.txt Tue Sep 12 09:49:30 2017 +0200 @@ -9,6 +9,7 @@ cubicweb/dataimport/test/test_csv.py cubicweb/dataimport/test/test_pgstore.py cubicweb/dataimport/test/test_massive_store.py +cubicweb/dataimport/test/test_sqlgenstore.py cubicweb/dataimport/test/test_stores.py cubicweb/dataimport/test/unittest_importer.py cubicweb/devtools/test/data/cubes/i18ntestcube/__init__.py @@ -42,6 +43,8 @@ cubicweb/server/rqlannotation.py cubicweb/server/schema2sql.py cubicweb/server/session.py +cubicweb/server/sources/__init__.py +cubicweb/server/sources/native.py cubicweb/server/sqlutils.py cubicweb/server/utils.py cubicweb/server/test/datacomputed/migratedapp/schema.py @@ -65,8 +68,6 @@ cubicweb/sobjects/test/unittest_notification.py cubicweb/sobjects/test/unittest_register_user.py cubicweb/sobjects/textparsers.py -cubicweb/sources/__init__.py -cubicweb/sources/native.py cubicweb/test/data/libpython/cubicweb_comment/__init__.py cubicweb/test/data/libpython/cubicweb_comment/__pkginfo__.py cubicweb/test/data/libpython/cubicweb_email/entities.py @@ -94,6 +95,7 @@ cubicweb/test/unittest_mail.py cubicweb/test/unittest_repoapi.py cubicweb/test/unittest_req.py +cubicweb/test/unittest_rqlrewrite.py cubicweb/test/unittest_rtags.py cubicweb/test/unittest_schema.py cubicweb/test/unittest_toolsutils.py