merge 3.16 fix in default
authorPierre-Yves David <pierre-yves.david@logilab.fr>
Thu, 21 Mar 2013 19:08:07 +0100
changeset 8759 cd68cd879def
parent 8752 e19f4bba89cd (current diff)
parent 8758 3a0d91237e2c (diff)
child 8760 17994bf95d6a
merge 3.16 fix in default
--- a/dbapi.py	Tue Mar 12 12:34:07 2013 +0100
+++ b/dbapi.py	Thu Mar 21 19:08:07 2013 +0100
@@ -204,13 +204,13 @@
       there goes authentication tokens. You usually have to specify a password
       for the given user, using a named 'password' argument.
     """
-    if urlparse(database).scheme is None:
+    if not urlparse(database).scheme:
         warn('[3.16] give an qualified URI as database instead of using '
              'host/cnxprops to specify the connection method',
              DeprecationWarning, stacklevel=2)
-        if cnxprops.cnxtype == 'zmq':
+        if cnxprops and cnxprops.cnxtype == 'zmq':
             database = kwargs.pop('host')
-        elif cnxprops.cnxtype == 'inmemory':
+        elif cnxprops and cnxprops.cnxtype == 'inmemory':
             database = 'inmemory://' + database
         else:
             database = 'pyro://%s/%s.%s' % (kwargs.pop('host', ''),
--- a/devtools/data/xvfb-run.sh	Tue Mar 12 12:34:07 2013 +0100
+++ b/devtools/data/xvfb-run.sh	Thu Mar 21 19:08:07 2013 +0100
@@ -140,7 +140,7 @@
 fi
 
 # tidy up after ourselves
-trap clean_up EXIT
+trap clean_up EXIT TERM
 
 # If the user did not specify an X authorization file to use, set up a temporary
 # directory to house one.
@@ -178,13 +178,8 @@
     exit 1
 done
 
-# Start the command and save its exit status.
-set +e
-DISPLAY=:$SERVERNUM XAUTHORITY=$AUTHFILE "$@" 2>&1
-RETVAL=$?
-set -e
-
-# Return the executed command's exit status.
-exit $RETVAL
+# Start the command
+DISPLAY=:$SERVERNUM XAUTHORITY=$AUTHFILE "$@" 2>&1 &
+wait $!
 
 # vim:set ai et sts=4 sw=4 tw=80:
--- a/devtools/qunit.py	Tue Mar 12 12:34:07 2013 +0100
+++ b/devtools/qunit.py	Thu Mar 21 19:08:07 2013 +0100
@@ -17,7 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
 import os, os.path as osp
-import signal
 from tempfile import mkdtemp, NamedTemporaryFile, TemporaryFile
 import tempfile
 from Queue import Queue, Empty
@@ -67,7 +66,6 @@
         self._tmp_dir = mkdtemp(prefix='cwtest-ffxprof-')
         self._profile_data = {'uid': uuid4()}
         self._profile_name = self.profile_name_mask % self._profile_data
-        fnull = open(os.devnull, 'w')
         stdout = TemporaryFile()
         stderr = TemporaryFile()
         self.firefox_cmd = ['firefox', '-no-remote']
@@ -81,7 +79,7 @@
                    ' Are you sure "%s" is a valid home  for user "%s"' % (home, user)
             check_call(self.firefox_cmd + ['-CreateProfile',
                         '%s %s' % (self._profile_name, self._tmp_dir)],
-                                  stdout=stdout, stderr=stderr)
+                                   stdout=stdout, stderr=stderr)
         except CalledProcessError as cpe:
             stdout.seek(0)
             stderr.seek(0)
@@ -96,7 +94,7 @@
     def stop(self):
         if self._process is not None:
             assert self._process.returncode is None,  self._process.returncode
-            os.kill(self._process.pid, signal.SIGTERM)
+            self._process.terminate()
             self._process.wait()
             self._process = None
 
--- a/doc/book/en/annexes/faq.rst	Tue Mar 12 12:34:07 2013 +0100
+++ b/doc/book/en/annexes/faq.rst	Thu Mar 21 19:08:07 2013 +0100
@@ -3,11 +3,6 @@
 Frequently Asked Questions (FAQ)
 ================================
 
-[XXX 'copy answer from forum' means reusing text from
-http://groups.google.com/group/google-appengine/browse_frm/thread/c9476925f5f66ec6
-and
-http://groups.google.com/group/google-appengine/browse_frm/thread/f48cf6099973aef5/c28cd6934dd72457
-]
 
 Generalities
 ````````````
--- a/server/__init__.py	Tue Mar 12 12:34:07 2013 +0100
+++ b/server/__init__.py	Thu Mar 21 19:08:07 2013 +0100
@@ -30,6 +30,7 @@
 from logilab.common.modutils import LazyObject
 from logilab.common.textutils import splitstrip
 from logilab.common.registry import yes
+from logilab import database
 
 from yams import BASE_GROUPS
 
@@ -162,7 +163,8 @@
     from cubicweb.dbapi import in_memory_repo_cnx
     from cubicweb.server.repository import Repository
     from cubicweb.server.utils import manager_userpasswd
-    from cubicweb.server.sqlutils import sqlexec, sqlschema, sqldropschema
+    from cubicweb.server.sqlutils import sqlexec, sqlschema, sql_drop_all_user_tables
+    from cubicweb.server.sqlutils import _SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION as drop_filter
     # configuration to avoid db schema loading and user'state checking
     # on connection
     config.creating = True
@@ -179,13 +181,21 @@
     sqlcursor = sqlcnx.cursor()
     execute = sqlcursor.execute
     if drop:
-        _title = '-> drop tables '
-        dropsql = sqldropschema(schema, driver)
-        try:
-            sqlexec(dropsql, execute, pbtitle=_title)
-        except Exception as ex:
-            print '-> drop failed, skipped (%s).' % ex
-            sqlcnx.rollback()
+        helper = database.get_db_helper(driver)
+        dropsql = sql_drop_all_user_tables(helper, sqlcursor)
+        # We may fail dropping some tables because of table dependencies, in a first pass.
+        # So, we try a second drop sequence to drop remaining tables if needed.
+        # Note that 2 passes is an arbitrary choice as it seems enougth for our usecases.
+        # (looping may induce infinite recursion when user have no right for example)
+        # Here we try to keep code simple and backend independant. That why we don't try to
+        # distinguish remaining tables (wrong right, dependencies, ...).
+        failed = sqlexec(dropsql, execute, cnx=sqlcnx,
+                         pbtitle='-> dropping tables (first pass)')
+        if failed:
+            failed = sqlexec(failed, execute, cnx=sqlcnx,
+                             pbtitle='-> dropping tables (second pass)')
+            remainings = filter(drop_filter, helper.list_tables(sqlcursor))
+            assert not remainings, 'Remaining tables: %s' % ', '.join(remainings)
     _title = '-> creating tables '
     print _title,
     # schema entities and relations tables
--- a/server/sqlutils.py	Tue Mar 12 12:34:07 2013 +0100
+++ b/server/sqlutils.py	Thu Mar 21 19:08:07 2013 +0100
@@ -20,8 +20,10 @@
 __docformat__ = "restructuredtext en"
 
 import os
+import re
 import subprocess
 from datetime import datetime, date
+from itertools import ifilter
 
 from logilab import database as db, common as lgc
 from logilab.common.shellutils import ProgressBar
@@ -49,27 +51,53 @@
 
 
 def sqlexec(sqlstmts, cursor_or_execute, withpb=not os.environ.get('APYCOT_ROOT'),
-            pbtitle='', delimiter=';'):
+            pbtitle='', delimiter=';', cnx=None):
     """execute sql statements ignoring DROP/ CREATE GROUP or USER statements
-    error. If a cnx is given, commit at each statement
+    error.
+
+    :sqlstmts_as_string: a string or a list of sql statements.
+    :cursor_or_execute: sql cursor or a callback used to execute statements
+    :cnx: if given, commit/rollback at each statement.
+
+    :withpb: if True, display a progresse bar
+    :pbtitle: a string displayed as the progress bar title (if `withpb=True`)
+
+    :delimiter: a string used to split sqlstmts (if it is a string)
+
+    Return the failed statements (same type as sqlstmts)
     """
     if hasattr(cursor_or_execute, 'execute'):
         execute = cursor_or_execute.execute
     else:
         execute = cursor_or_execute
-    sqlstmts = sqlstmts.split(delimiter)
+    sqlstmts_as_string = False
+    if isinstance(sqlstmts, basestring):
+        sqlstmts_as_string = True
+        sqlstmts = sqlstmts.split(delimiter)
     if withpb:
         pb = ProgressBar(len(sqlstmts), title=pbtitle)
+    failed = []
     for sql in sqlstmts:
         sql = sql.strip()
         if withpb:
             pb.update()
         if not sql:
             continue
-        # some dbapi modules doesn't accept unicode for sql string
-        execute(str(sql))
+        try:
+            # some dbapi modules doesn't accept unicode for sql string
+            execute(str(sql))
+        except Exception, err:
+            if cnx:
+                cnx.rollback()
+            failed.append(sql)
+        else:
+            if cnx:
+                cnx.commit()
     if withpb:
         print
+    if sqlstmts_as_string:
+        failed = delimiter.join(failed)
+    return failed
 
 
 def sqlgrants(schema, driver, user,
@@ -137,6 +165,23 @@
     return '\n'.join(output)
 
 
+_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION = re.compile('^(?!(sql|pg)_)').match
+def sql_drop_all_user_tables(driver_or_helper, sqlcursor):
+    """Return ths sql to drop all tables found in the database system."""
+    if not getattr(driver_or_helper, 'list_tables', None):
+        dbhelper = db.get_db_helper(driver_or_helper)
+    else:
+        dbhelper = driver_or_helper
+
+    cmds = [dbhelper.sql_drop_sequence('entities_id_seq')]
+    # for mssql, we need to drop views before tables
+    if hasattr(dbhelper, 'list_views'):
+        cmds += ['DROP VIEW %s;' % name
+                 for name in ifilter(_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION, dbhelper.list_views(sqlcursor))]
+    cmds += ['DROP TABLE %s;' % name
+             for name in ifilter(_SQL_DROP_ALL_USER_TABLES_FILTER_FUNCTION, dbhelper.list_tables(sqlcursor))]
+    return '\n'.join(cmds)
+
 class SQLAdapterMixIn(object):
     """Mixin for SQL data sources, getting a connection from a configuration
     dictionary and handling connection locking
--- a/web/application.py	Tue Mar 12 12:34:07 2013 +0100
+++ b/web/application.py	Thu Mar 21 19:08:07 2013 +0100
@@ -485,6 +485,7 @@
             raise
         ### Last defense line
         except BaseException as ex:
+            req.status_out = httplib.INTERNAL_SERVER_ERROR
             result = self.error_handler(req, ex, tb=True)
         finally:
             if req.cnx and not commited:
@@ -538,6 +539,7 @@
             self.exception(repr(ex))
         req.set_header('Cache-Control', 'no-cache')
         req.remove_header('Etag')
+        req.remove_header('Content-disposition')
         req.reset_message()
         req.reset_headers()
         if req.ajax_request:
--- a/web/test/unittest_idownloadable.py	Tue Mar 12 12:34:07 2013 +0100
+++ b/web/test/unittest_idownloadable.py	Thu Mar 21 19:08:07 2013 +0100
@@ -25,29 +25,40 @@
 from cubicweb import view
 from cubicweb.predicates import is_instance
 
+class IDownloadableUser(view.EntityAdapter):
+    __regid__ = 'IDownloadable'
+    __select__ = is_instance('CWUser')
+
+    def download_content_type(self):
+        """return MIME type of the downloadable content"""
+        return 'text/plain'
+
+    def download_encoding(self):
+        """return encoding of the downloadable content"""
+        return 'ascii'
+
+    def download_file_name(self):
+        """return file name of the downloadable content"""
+        return  self.entity.name() + '.txt'
+
+    def download_data(self):
+        return 'Babar is not dead!'
+
+
+class BrokenIDownloadableGroup(IDownloadableUser):
+    __regid__ = 'IDownloadable'
+    __select__ = is_instance('CWGroup')
+
+    def download_file_name(self):
+        return  self.entity.name + '.txt'
+
+    def download_data(self):
+        raise IOError()
 
 class IDownloadableTC(CubicWebTC):
 
     def setUp(self):
         super(IDownloadableTC, self).setUp()
-        class IDownloadableUser(view.EntityAdapter):
-            __regid__ = 'IDownloadable'
-            __select__ = is_instance('CWUser')
-
-            def download_content_type(self):
-                """return MIME type of the downloadable content"""
-                return 'text/plain'
-
-            def download_encoding(self):
-                """return encoding of the downloadable content"""
-                return 'ascii'
-
-            def download_file_name(self):
-                """return file name of the downloadable content"""
-                return  self.entity.name() + '.txt'
-
-            def download_data(self):
-                return 'Babar is not dead!'
         self.vreg.register(IDownloadableUser)
         self.addCleanup(partial(self.vreg.unregister, IDownloadableUser))
 
@@ -55,7 +66,7 @@
         req = self.request()
         req.form['vid'] = 'download'
         req.form['eid'] = str(req.user.eid)
-        data = self.ctrl_publish(req,'view')
+        data = self.ctrl_publish(req, 'view')
         get = req.headers_out.getRawHeaders
         self.assertEqual(['attachment;filename="admin.txt"'],
                          get('content-disposition'))
@@ -122,5 +133,24 @@
             self.assertEqual(["""attachment;filename="Brte_h_grand_nm_a_va_totallement_dborder_de_la_limite_l.txt";filename*=utf-8''B%C3%A8rte_h%C3%B4_grand_n%C3%B4m_%C3%A7a_va_totallement_d%C3%A9border_de_la_limite_l%C3%A0.txt"""],
                              get('content-disposition'))
 
+
+    def test_download_data_error(self):
+        self.vreg.register(BrokenIDownloadableGroup)
+        self.addCleanup(partial(self.vreg.unregister, BrokenIDownloadableGroup))
+        req = self.request()
+        req.form['vid'] = 'download'
+        req.form['eid'] = str(req.execute('CWGroup X WHERE X name "managers"')[0][0])
+        errhdlr = self.app.__dict__.pop('error_handler') # temporarily restore error handler
+        try:
+            data = self.app_handle_request(req)
+        finally:
+            self.app.error_handler = errhdlr
+        get = req.headers_out.getRawHeaders
+        self.assertEqual(['application/xhtml+xml'],
+                         get('content-type'))
+        self.assertEqual(None,
+                         get('content-disposition'))
+        self.assertEqual(req.status_out, 500)
+
 if __name__ == '__main__':
     unittest_main()
--- a/web/views/baseviews.py	Tue Mar 12 12:34:07 2013 +0100
+++ b/web/views/baseviews.py	Thu Mar 21 19:08:07 2013 +0100
@@ -565,8 +565,9 @@
         w = self.w
         w(u'<ul class="boxListing">')
         for key in displayed:
-            w(u'<li>%s</li>\n' %
-              self.index_link(basepath, key, index[key]))
+            if key:
+                w(u'<li>%s</li>\n' %
+                  self.index_link(basepath, key, index[key]))
         if needmore:
             url = self._cw.build_url('view', vid=self.__regid__,
                                      rql=self.cw_rset.printable_rql())
@@ -616,6 +617,8 @@
         return (None, None)
 
     def index_link(self, basepath, key, items):
+        if key[0] is None:
+            return
         label = u'%s [%s]' % (key[0], len(items))
         etypes = set(entity.__regid__ for entity in items)
         vtitle = self._cw._('%(etype)s by %(author)s') % {