# HG changeset patch # User Sylvain Thénault # Date 1286366666 -7200 # Node ID d7f5d873e1b86eb5bb6e22ef09ba8bee36c53e7a # Parent 21468682f68881d7e6b1b9b21db363f2b7b6bd45# Parent 5f08485e3b112d79fbe3e9710e257a4b3c542d46 backport stable diff -r 21468682f688 -r d7f5d873e1b8 debian/control --- a/debian/control Wed Oct 06 11:57:21 2010 +0200 +++ b/debian/control Wed Oct 06 14:04:26 2010 +0200 @@ -34,7 +34,7 @@ Replaces: cubicweb-multisources Provides: cubicweb-multisources Depends: ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.3.1), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2 -Recommends: pyro, cubicweb-documentation (= ${source:Version}) +Recommends: pyro (< 4.0.0), cubicweb-documentation (= ${source:Version}) Description: server part of the CubicWeb framework CubicWeb is a semantic web application framework. . @@ -69,7 +69,7 @@ XB-Python-Version: ${python:Versions} Provides: cubicweb-web-frontend Depends: ${python:Depends}, cubicweb-web (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-twisted-web -Recommends: pyro, cubicweb-documentation (= ${source:Version}) +Recommends: pyro (< 4.0.0), cubicweb-documentation (= ${source:Version}) Description: twisted-based web interface for the CubicWeb framework CubicWeb is a semantic web application framework. . diff -r 21468682f688 -r d7f5d873e1b8 devtools/testlib.py --- a/devtools/testlib.py Wed Oct 06 11:57:21 2010 +0200 +++ b/devtools/testlib.py Wed Oct 06 14:04:26 2010 +0200 @@ -45,6 +45,7 @@ from cubicweb.sobjects import notification from cubicweb.web import Redirect, application from cubicweb.server.session import security_enabled +from cubicweb.server.hook import SendMailOp from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS from cubicweb.devtools import BASE_URL, fake, htmlparser from cubicweb.utils import json @@ -291,6 +292,9 @@ # default test setup and teardown ######################################### def setUp(self): + # monkey patch send mail operation so emails are sent synchronously + self._old_mail_commit_event = SendMailOp.commit_event + SendMailOp.commit_event = SendMailOp.sendmails pause_tracing() previous_failure = self.__class__.__dict__.get('_repo_init_failed') if previous_failure is not None: @@ -312,6 +316,7 @@ for cnx in self._cnxs: if not cnx._closed: cnx.close() + SendMailOp.commit_event = self._old_mail_commit_event def setup_database(self): """add your database setup code by overriding this method""" diff -r 21468682f688 -r d7f5d873e1b8 doc/book/en/devrepo/devcore/dbapi.rst --- a/doc/book/en/devrepo/devcore/dbapi.rst Wed Oct 06 11:57:21 2010 +0200 +++ b/doc/book/en/devrepo/devcore/dbapi.rst Wed Oct 06 14:04:26 2010 +0200 @@ -23,10 +23,11 @@ .. note:: If a query generates an error related to security (:exc:`Unauthorized`) or to - integrity (:exc:`ValidationError`), a rollback is automatically done on the - current transaction. + integrity (:exc:`ValidationError`), the transaction can still continue but you + won't be able to commit it, a rollback will be necessary to start a new + transaction. - Also, a rollback is done if an error occurs during commit. + Also, a rollback is automatically done if an error occurs during commit. Executing RQL queries from a view or a hook diff -r 21468682f688 -r d7f5d873e1b8 server/querier.py --- a/server/querier.py Wed Oct 06 11:57:21 2010 +0200 +++ b/server/querier.py Wed Oct 06 14:04:26 2010 +0200 @@ -712,7 +712,7 @@ # * don't rollback if we're in the commit process, will be handled # by the session if session.commit_state is None: - session.rollback(reset_pool=False) + session.commit_state = 'uncommitable' raise # build a description for the results if necessary descr = () diff -r 21468682f688 -r d7f5d873e1b8 server/session.py --- a/server/session.py Wed Oct 06 11:57:21 2010 +0200 +++ b/server/session.py Wed Oct 06 14:04:26 2010 +0200 @@ -31,7 +31,7 @@ from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj from yams import BASE_TYPES -from cubicweb import Binary, UnknownEid, schema +from cubicweb import Binary, UnknownEid, QueryError, schema from cubicweb.req import RequestSessionBase from cubicweb.dbapi import ConnectionProperties from cubicweb.utils import make_uid, RepeatList @@ -738,7 +738,10 @@ self._touch() self.debug('commit session %s done (no db activity)', self.id) return - if self.commit_state: + cstate = self.commit_state + if cstate == 'uncommitable': + raise QueryError('transaction must be rollbacked') + if cstate is not None: return # on rollback, an operation should have the following state # information: diff -r 21468682f688 -r d7f5d873e1b8 server/sources/storages.py --- a/server/sources/storages.py Wed Oct 06 11:57:21 2010 +0200 +++ b/server/sources/storages.py Wed Oct 06 14:04:26 2010 +0200 @@ -18,6 +18,7 @@ """custom storages for the system source""" from os import unlink, path as osp +from contextlib import contextmanager from yams.schema import role_name @@ -96,6 +97,17 @@ return path return None +@contextmanager +def fsimport(session): + present = 'fs_importing' in session.transaction_data + old_value = session.transaction_data.get('fs_importing') + session.transaction_data['fs_importing'] = True + yield + if present: + session.transaction_data['fs_importing'] = old_value + else: + del session.transaction_data['fs_importing'] + class BytesFileSystemStorage(Storage): """store Bytes attribute value on the file system""" diff -r 21468682f688 -r d7f5d873e1b8 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Wed Oct 06 11:57:21 2010 +0200 +++ b/server/test/unittest_repository.py Wed Oct 06 14:04:26 2010 +0200 @@ -32,7 +32,7 @@ from yams.constraints import UniqueConstraint from cubicweb import (BadConnectionId, RepositoryError, ValidationError, - UnknownEid, AuthenticationError, Unauthorized) + UnknownEid, AuthenticationError, Unauthorized, QueryError) from cubicweb.selectors import is_instance from cubicweb.schema import CubicWebSchema, RQLConstraint from cubicweb.dbapi import connect, multiple_connections_unfix @@ -154,6 +154,10 @@ with self.temporary_appobjects(ValidationErrorAfterHook): self.assertRaises(ValidationError, self.execute, 'SET X name "toto" WHERE X is CWGroup, X name "guests"') + self.failUnless(self.execute('Any X WHERE X is CWGroup, X name "toto"')) + ex = self.assertRaises(QueryError, self.commit) + self.assertEqual(str(ex), 'transaction must be rollbacked') + self.rollback() self.failIf(self.execute('Any X WHERE X is CWGroup, X name "toto"')) def test_rollback_on_execute_unauthorized(self): @@ -166,6 +170,10 @@ with self.temporary_appobjects(UnauthorizedAfterHook): self.assertRaises(Unauthorized, self.execute, 'SET X name "toto" WHERE X is CWGroup, X name "guests"') + self.failUnless(self.execute('Any X WHERE X is CWGroup, X name "toto"')) + ex = self.assertRaises(QueryError, self.commit) + self.assertEqual(str(ex), 'transaction must be rollbacked') + self.rollback() self.failIf(self.execute('Any X WHERE X is CWGroup, X name "toto"')) diff -r 21468682f688 -r d7f5d873e1b8 server/test/unittest_storage.py --- a/server/test/unittest_storage.py Wed Oct 06 11:57:21 2010 +0200 +++ b/server/test/unittest_storage.py Wed Oct 06 14:04:26 2010 +0200 @@ -19,7 +19,7 @@ from __future__ import with_statement -from logilab.common.testlib import unittest_main, tag +from logilab.common.testlib import unittest_main, tag, Tags from cubicweb.devtools.testlib import CubicWebTC import os.path as osp @@ -52,6 +52,8 @@ class StorageTC(CubicWebTC): + tags = CubicWebTC.tags | Tags('Storage', 'BFSS') + def setup_database(self): self.tempdir = tempfile.mkdtemp() bfs_storage = storages.BytesFileSystemStorage(self.tempdir) @@ -184,7 +186,7 @@ self.assertEqual(f1.data.getvalue(), file(filepath).read(), 'files content differ') - @tag('Storage', 'BFSS', 'update') + @tag('update') def test_bfss_update_with_existing_data(self): # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), @@ -198,7 +200,7 @@ f2 = self.execute('Any F WHERE F eid %(f)s, F is File', {'f': f1.eid}).get_entity(0, 0) self.assertEqual(f2.data.getvalue(), 'some other data') - @tag('Storage', 'BFSS', 'update', 'extension', 'commit') + @tag('update', 'extension', 'commit') def test_bfss_update_with_different_extension_commited(self): # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), @@ -220,7 +222,7 @@ self.failUnless(osp.isfile(new_path)) self.assertEqual(osp.splitext(new_path)[1], '.jpg') - @tag('Storage', 'BFSS', 'update', 'extension', 'rollback') + @tag('update', 'extension', 'rollback') def test_bfss_update_with_different_extension_rollbacked(self): # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), @@ -245,6 +247,7 @@ self.assertEqual(old_path, new_path) self.assertEqual(old_data, new_data) + @tag('fs_importing', 'update') def test_bfss_update_with_fs_importing(self): # use self.session to use server-side cache f1 = self.session.create_entity('File', data=Binary('some data'), @@ -260,6 +263,35 @@ self.assertEqual(self.fspath(f1), new_fspath) self.failIf(osp.isfile(old_fspath)) + @tag('fsimport') + def test_clean(self): + fsimport = storages.fsimport + td = self.session.transaction_data + self.assertNotIn('fs_importing', td) + with fsimport(self.session): + self.assertIn('fs_importing', td) + self.assertTrue(td['fs_importing']) + self.assertNotIn('fs_importing', td) + + @tag('fsimport') + def test_true(self): + fsimport = storages.fsimport + td = self.session.transaction_data + td['fs_importing'] = True + with fsimport(self.session): + self.assertIn('fs_importing', td) + self.assertTrue(td['fs_importing']) + self.assertTrue(td['fs_importing']) + + @tag('fsimport') + def test_False(self): + fsimport = storages.fsimport + td = self.session.transaction_data + td['fs_importing'] = False + with fsimport(self.session): + self.assertIn('fs_importing', td) + self.assertTrue(td['fs_importing']) + self.assertFalse(td['fs_importing']) if __name__ == '__main__': unittest_main() diff -r 21468682f688 -r d7f5d873e1b8 server/utils.py --- a/server/utils.py Wed Oct 06 11:57:21 2010 +0200 +++ b/server/utils.py Wed Oct 06 14:04:26 2010 +0200 @@ -117,6 +117,14 @@ sconfig.input_config(inputlevel=inputlevel) return sconfig +_MARKER=object() +def func_name(func): + name = getattr(func, '__name__', _MARKER) + if name is _MARKER: + name = getattr(func, 'func_name', _MARKER) + if name is _MARKER: + name = repr(func) + return name class LoopTask(object): """threaded task restarting itself once executed""" @@ -124,7 +132,7 @@ if interval <= 0: raise ValueError('Loop task interval must be > 0 ' '(current value: %f for %s)' % \ - (interval, func.__name__)) + (interval, func_name(func))) self.interval = interval def auto_restart_func(self=self, func=func, args=args): try: @@ -132,7 +140,7 @@ finally: self.start() self.func = auto_restart_func - self.name = func.__name__ + self.name = func_name(func) def __str__(self): return '%s (%s seconds)' % (self.name, self.interval) @@ -162,7 +170,7 @@ self.running_threads.remove(self) Thread.__init__(self, target=auto_remove_func) self.running_threads = running_threads - self._name = target.__name__ + self._name = func_name(target) def start(self): self.running_threads.append(self) diff -r 21468682f688 -r d7f5d873e1b8 vregistry.py --- a/vregistry.py Wed Oct 06 11:57:21 2010 +0200 +++ b/vregistry.py Wed Oct 06 14:04:26 2010 +0200 @@ -240,8 +240,8 @@ msg = 'select ambiguity: %s\n(args: %s, kwargs: %s)' if self.config.debugmode or self.config.mode == 'test': # raise bare exception in debug mode - raise Exception(msg % (winners, self.args, self.kwargs.keys())) - self.error(msg, winners, self.args, self.kwargs.keys()) + raise Exception(msg % (winners, args, kwargs.keys())) + self.error(msg, winners, args, kwargs.keys()) # return the result of calling the appobject return winners[0](*args, **kwargs) diff -r 21468682f688 -r d7f5d873e1b8 web/views/autoform.py --- a/web/views/autoform.py Wed Oct 06 11:57:21 2010 +0200 +++ b/web/views/autoform.py Wed Oct 06 14:04:26 2010 +0200 @@ -941,10 +941,11 @@ global etype_relation_field def etype_relation_field(etype, rtype, role='subject'): - eschema = vreg.schema.eschema(etype) try: + eschema = vreg.schema.eschema(etype) return AutomaticEntityForm.field_by_name(rtype, role, eschema) - except f.FieldNotFound: + except (KeyError, f.FieldNotFound): + # catch KeyError raised when etype/rtype not found in schema AutomaticEntityForm.error('field for %s %s may not be found in schema' % (rtype, role)) return None diff -r 21468682f688 -r d7f5d873e1b8 web/views/urlrewrite.py --- a/web/views/urlrewrite.py Wed Oct 06 11:57:21 2010 +0200 +++ b/web/views/urlrewrite.py Wed Oct 06 14:04:26 2010 +0200 @@ -163,7 +163,7 @@ return do_build_rset def rgx_action(rql=None, args=None, cachekey=None, argsgroups=(), setuser=False, - form=None, formgroups=(), transforms={}, controller=None): + form=None, formgroups=(), transforms={}, rqlformparams=(), controller=None): def do_build_rset(inputurl, uri, req, schema, cachekey=cachekey # necessary to avoid UnboundLocalError ): @@ -183,6 +183,8 @@ kwargs[key] = typed_eid(value) if setuser: kwargs['u'] = req.user.eid + for param in rqlformparams: + kwargs.setdefault(param, req.form.get(param)) rset = req.execute(rql, kwargs, cachekey) else: rset = None