backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 06 Oct 2010 14:04:26 +0200
changeset 6401 d7f5d873e1b8
parent 6400 21468682f688 (current diff)
parent 6399 5f08485e3b11 (diff)
child 6402 a589df8b9b66
backport stable
debian/control
devtools/testlib.py
server/querier.py
server/session.py
server/sources/storages.py
server/test/unittest_repository.py
vregistry.py
web/views/autoform.py
--- 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.
  .
--- 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"""
--- 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
--- 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 = ()
--- 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:
--- 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"""
--- 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"'))
 
 
--- 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()
--- 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)
--- 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)
 
--- 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
 
--- 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