# HG changeset patch # User Aurelien Campeas # Date 1357743980 -3600 # Node ID 310040c668c067bbee84e47bd1b30e3349d1382b # Parent 97202ea671e47a23206569b51ec54eb940b91736# Parent 459d0c48dfafee903c15a5349d321f6e8f998cbb [merge] backport stable diff -r 97202ea671e4 -r 310040c668c0 .hgtags --- a/.hgtags Wed Jan 09 15:46:05 2013 +0100 +++ b/.hgtags Wed Jan 09 16:06:20 2013 +0100 @@ -270,3 +270,5 @@ 19e115ae5442c427c0adbda8b9d8ceccf2931b5c cubicweb-debian-version-3.15.5-1 0163bd9f4880d5531e433c1500f9298a0adef6b7 cubicweb-version-3.15.6 b05e156b8fe720494293b08e7060ba43ad57a5c8 cubicweb-debian-version-3.15.6-1 +d8916cee7b705fec66fa2797ab89ba3e3b617ced cubicweb-version-3.15.7 +c5400558f37079a8bf6f2cd27a1ffd49321f3d8b cubicweb-debian-version-3.15.7-1 diff -r 97202ea671e4 -r 310040c668c0 __pkginfo__.py --- a/__pkginfo__.py Wed Jan 09 15:46:05 2013 +0100 +++ b/__pkginfo__.py Wed Jan 09 16:06:20 2013 +0100 @@ -22,7 +22,7 @@ modname = distname = "cubicweb" -numversion = (3, 15, 6) +numversion = (3, 15, 8) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" diff -r 97202ea671e4 -r 310040c668c0 debian/changelog --- a/debian/changelog Wed Jan 09 15:46:05 2013 +0100 +++ b/debian/changelog Wed Jan 09 16:06:20 2013 +0100 @@ -1,3 +1,15 @@ +cubicweb (3.15.8-1) squeeze; urgency=low + + * New upstream release + + -- Aurélien Campéas Wed, 09 Jan 2013 15:40:00 +0100 + +cubicweb (3.15.7-1) squeeze; urgency=low + + * New upstream release + + -- David Douard Wed, 12 Dec 2012 22:10:45 +0100 + cubicweb (3.15.6-1) squeeze; urgency=low * New upstream release diff -r 97202ea671e4 -r 310040c668c0 devtools/testlib.py --- a/devtools/testlib.py Wed Jan 09 15:46:05 2013 +0100 +++ b/devtools/testlib.py Wed Jan 09 16:06:20 2013 +0100 @@ -815,7 +815,11 @@ :returns: an instance of `cubicweb.devtools.htmlparser.PageInfo` encapsulation the generated HTML """ - req = req or rset and rset.req or self.request() + if req is None: + if rset is None: + req = self.request() + else: + req = rset.req req.form['vid'] = vid viewsreg = self.vreg['views'] view = viewsreg.select(vid, req, rset=rset, **kwargs) diff -r 97202ea671e4 -r 310040c668c0 doc/book/en/admin/ldap.rst --- a/doc/book/en/admin/ldap.rst Wed Jan 09 15:46:05 2013 +0100 +++ b/doc/book/en/admin/ldap.rst Wed Jan 09 16:06:20 2013 +0100 @@ -81,13 +81,20 @@ Other notes ----------- -* Yes, cubicweb is able to start if ldap cannot be reached, even on c-c start, - though that will slow down the instance, since it will indefinitly attempt - to connect to the ldap on each query on users. +* Cubicweb is able to start if ldap cannot be reached, even on + cubicweb-ctl start ... If some source ldap server cannot be used + while an instance is running, the corresponding users won't be + authenticated but their status will not change (e.g. they will not + be deactivated) * Changing the name of the ldap server in your script is fine, changing the base DN isn't since it's used to identify already known users from others +* When a user is removed from an LDAP source, it is deactivated in the + CubicWeb instance; when a deactivated user comes back in the LDAP + source, it (automatically) is activated again + + * You can use the :class:`CWSourceHostConfig` to have variants for a source configuration according to the host the instance is running on. To do so go on the source's view from the sources management view. diff -r 97202ea671e4 -r 310040c668c0 doc/book/en/annexes/rql/language.rst --- a/doc/book/en/annexes/rql/language.rst Wed Jan 09 15:46:05 2013 +0100 +++ b/doc/book/en/annexes/rql/language.rst Wed Jan 09 16:06:20 2013 +0100 @@ -640,7 +640,7 @@ | :func:`FSPATH(X)` | expect X to be an attribute whose value is stored in a | | | :class:`BFSStorage` and return its path on the file system | +-----------------------+--------------------------------------------------------------------+ -| :func:`FTKIRANK(X)` | expect X to be an entity used in a has_text relation, and return a | +| :func:`FTIRANK(X)` | expect X to be an entity used in a has_text relation, and return a | | | number corresponding to the rank order of each resulting entity | +-----------------------+--------------------------------------------------------------------+ | :func:`CAST(Type, X)` | expect X to be an attribute and return it casted into the given | diff -r 97202ea671e4 -r 310040c668c0 misc/scripts/ldapuser2ldapfeed.py --- a/misc/scripts/ldapuser2ldapfeed.py Wed Jan 09 15:46:05 2013 +0100 +++ b/misc/scripts/ldapuser2ldapfeed.py Wed Jan 09 16:06:20 2013 +0100 @@ -80,7 +80,7 @@ pprint(duplicates) print len(todelete), 'entities will be deleted' -for etype, entities in todelete.values(): +for etype, entities in todelete.iteritems(): print 'deleting', etype, [e.login for e in entities] system_source.delete_info_multi(session, entities, source_name) diff -r 97202ea671e4 -r 310040c668c0 server/cwzmq.py --- a/server/cwzmq.py Wed Jan 09 15:46:05 2013 +0100 +++ b/server/cwzmq.py Wed Jan 09 16:06:20 2013 +0100 @@ -32,6 +32,23 @@ ctx = zmq.Context() class ZMQComm(object): + """ + A simple ZMQ-based notification bus. + + There should at most one instance of this class attached to a + Repository. A typical usage may be something like:: + + def callback(msg): + self.info('received message: %s', ' '.join(msg)) + repo.app_instances_bus.subscribe('hello', callback) + + to subsribe to the 'hello' kind of message. On the other side, to + emit a notification, call:: + + repo.app_instances_bus.publish(['hello', 'world']) + + See http://docs.cubicweb.org for more details. + """ def __init__(self): self.ioloop = ioloop.IOLoop() self._topics = {} diff -r 97202ea671e4 -r 310040c668c0 server/ldaputils.py --- a/server/ldaputils.py Wed Jan 09 15:46:05 2013 +0100 +++ b/server/ldaputils.py Wed Jan 09 16:06:20 2013 +0100 @@ -222,19 +222,6 @@ raise AuthenticationError() return eid - def object_exists_in_ldap(self, dn): - cnx = self.get_connection().cnx #session.cnxset.connection(self.uri).cnx - if cnx is None: - self.warning('Could not establish connexion with LDAP server, assuming dn %s exists', dn) - return True # ldap unreachable, let's not touch it - try: - cnx.search_s(dn, self.user_base_scope) - except ldap.PARTIAL_RESULTS: - self.warning('PARTIAL RESULTS for dn %s', dn) - except ldap.NO_SUCH_OBJECT: - return False - return True - def _connect(self, user=None, userpwd=None): protocol, hostport = self.connection_info() self.info('connecting %s://%s as %s', protocol, hostport, diff -r 97202ea671e4 -r 310040c668c0 server/migractions.py --- a/server/migractions.py Wed Jan 09 15:46:05 2013 +0100 +++ b/server/migractions.py Wed Jan 09 16:06:20 2013 +0100 @@ -1061,7 +1061,7 @@ rdef = copy(rschema.rdef(rschema.subjects(objtype)[0], objtype)) rdef.subject = etype rdef.rtype = self.repo.schema.rschema(rschema) - rdef.object = self.repo.schema.rschema(objtype) + rdef.object = self.repo.schema.eschema(objtype) ss.execschemarql(execute, rdef, ss.rdef2rql(rdef, cmap, gmap)) if commit: diff -r 97202ea671e4 -r 310040c668c0 server/serverconfig.py --- a/server/serverconfig.py Wed Jan 09 15:46:05 2013 +0100 +++ b/server/serverconfig.py Wed Jan 09 16:06:20 2013 +0100 @@ -1,4 +1,4 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This file is part of CubicWeb. @@ -292,7 +292,7 @@ return True return source.uri in self.sources_mode if self.quick_start: - return False + return source.uri == 'system' return (not source.disabled and ( not self.enabled_sources or source.uri in self.enabled_sources)) diff -r 97202ea671e4 -r 310040c668c0 server/sources/ldapuser.py --- a/server/sources/ldapuser.py Wed Jan 09 15:46:05 2013 +0100 +++ b/server/sources/ldapuser.py Wed Jan 09 16:06:20 2013 +0100 @@ -19,7 +19,7 @@ this source is for now limited to a read-only CWUser source """ -from __future__ import division +from __future__ import division, with_statement from base64 import b64decode import ldap @@ -114,7 +114,8 @@ self._query_cache.clear_expired) def synchronize(self): - self.pull_data(self.repo.internal_session()) + with self.repo.internal_session() as session: + self.pull_data(session) def pull_data(self, session, force=False, raise_on_error=False): """synchronize content known by this repository with content in the @@ -123,51 +124,47 @@ self.info('synchronizing ldap source %s', self.uri) ldap_emailattr = self.user_rev_attrs['email'] assert ldap_emailattr - session = self.repo.internal_session() execute = session.execute - try: - cursor = session.system_sql("SELECT eid, extid FROM entities WHERE " - "source='%s'" % self.uri) - for eid, b64extid in cursor.fetchall(): - extid = b64decode(b64extid) - self.debug('ldap eid %s', eid) - # if no result found, _search automatically delete entity information - res = self._search(session, extid, BASE) - self.debug('ldap search %s', res) - if res: - ldapemailaddr = res[0].get(ldap_emailattr) - if ldapemailaddr: - if isinstance(ldapemailaddr, list): - ldapemailaddr = ldapemailaddr[0] # XXX consider only the first email in the list - rset = execute('Any X,A WHERE ' - 'X address A, U use_email X, U eid %(u)s', - {'u': eid}) - ldapemailaddr = unicode(ldapemailaddr) - for emaileid, emailaddr, in rset: - if emailaddr == ldapemailaddr: - break + cursor = session.system_sql("SELECT eid, extid FROM entities WHERE " + "source='%s'" % self.uri) + for eid, b64extid in cursor.fetchall(): + extid = b64decode(b64extid) + self.debug('ldap eid %s', eid) + # if no result found, _search automatically delete entity information + res = self._search(session, extid, BASE) + self.debug('ldap search %s', res) + if res: + ldapemailaddr = res[0].get(ldap_emailattr) + if ldapemailaddr: + if isinstance(ldapemailaddr, list): + ldapemailaddr = ldapemailaddr[0] # XXX consider only the first email in the list + rset = execute('Any X,A WHERE ' + 'X address A, U use_email X, U eid %(u)s', + {'u': eid}) + ldapemailaddr = unicode(ldapemailaddr) + for emaileid, emailaddr, in rset: + if emailaddr == ldapemailaddr: + break + else: + self.debug('updating email address of user %s to %s', + extid, ldapemailaddr) + emailrset = execute('EmailAddress A WHERE A address %(addr)s', + {'addr': ldapemailaddr}) + if emailrset: + execute('SET U use_email X WHERE ' + 'X eid %(x)s, U eid %(u)s', + {'x': emailrset[0][0], 'u': eid}) + elif rset: + if not execute('SET X address %(addr)s WHERE ' + 'U primary_email X, U eid %(u)s', + {'addr': ldapemailaddr, 'u': eid}): + execute('SET X address %(addr)s WHERE ' + 'X eid %(x)s', + {'addr': ldapemailaddr, 'x': rset[0][0]}) else: - self.debug('updating email address of user %s to %s', - extid, ldapemailaddr) - emailrset = execute('EmailAddress A WHERE A address %(addr)s', - {'addr': ldapemailaddr}) - if emailrset: - execute('SET U use_email X WHERE ' - 'X eid %(x)s, U eid %(u)s', - {'x': emailrset[0][0], 'u': eid}) - elif rset: - if not execute('SET X address %(addr)s WHERE ' - 'U primary_email X, U eid %(u)s', - {'addr': ldapemailaddr, 'u': eid}): - execute('SET X address %(addr)s WHERE ' - 'X eid %(x)s', - {'addr': ldapemailaddr, 'x': rset[0][0]}) - else: - # no email found, create it - _insert_email(session, ldapemailaddr, eid) - finally: - session.commit() - session.close() + # no email found, create it + _insert_email(session, ldapemailaddr, eid) + session.commit() def ldap_name(self, var): if var.stinfo['relations']: diff -r 97202ea671e4 -r 310040c668c0 server/test/unittest_ldapuser.py --- a/server/test/unittest_ldapuser.py Wed Jan 09 15:46:05 2013 +0100 +++ b/server/test/unittest_ldapuser.py Wed Jan 09 16:06:20 2013 +0100 @@ -103,9 +103,9 @@ session.create_entity('CWSource', name=u'ldapuser', type=u'ldapfeed', parser=u'ldapfeed', url=URL, config=CONFIG) session.commit() - isession = session.repo.internal_session(safe=True) - lfsource = isession.repo.sources_by_uri['ldapuser'] - stats = lfsource.pull_data(isession, force=True, raise_on_error=True) + with session.repo.internal_session(safe=True) as isession: + lfsource = isession.repo.sources_by_uri['ldapuser'] + stats = lfsource.pull_data(isession, force=True, raise_on_error=True) def _pull(self): with self.session.repo.internal_session() as isession: @@ -113,6 +113,34 @@ stats = lfsource.pull_data(isession, force=True, raise_on_error=True) isession.commit() + def test_a_filter_inactivate(self): + """ filtered out people should be deactivated, unable to authenticate """ + source = self.session.execute('CWSource S WHERE S type="ldapfeed"').get_entity(0,0) + config = source.repo_source.check_config(source) + # filter with adim's phone number + config['user-filter'] = u'(%s=%s)' % ('telephoneNumber', '109') + source.repo_source.update_config(source, config) + self.commit() + self._pull() + self.assertRaises(AuthenticationError, self.repo.connect, 'syt', password='syt') + self.assertEqual(self.execute('Any N WHERE U login "syt", ' + 'U in_state S, S name N').rows[0][0], + 'deactivated') + self.assertEqual(self.execute('Any N WHERE U login "adim", ' + 'U in_state S, S name N').rows[0][0], + 'activated') + # unfilter, syt should be activated again + config['user-filter'] = u'' + source.repo_source.update_config(source, config) + self.commit() + self._pull() + self.assertEqual(self.execute('Any N WHERE U login "syt", ' + 'U in_state S, S name N').rows[0][0], + 'activated') + self.assertEqual(self.execute('Any N WHERE U login "adim", ' + 'U in_state S, S name N').rows[0][0], + 'activated') + def test_delete(self): """ delete syt, pull, check deactivation, repull, readd syt, pull, check activation @@ -132,10 +160,9 @@ self.tearDownClass() self.setUpClass() self._pull() - # still deactivated, but a warning has been emitted ... self.assertEqual(self.execute('Any N WHERE U login "syt", ' 'U in_state S, S name N').rows[0][0], - 'deactivated') + 'activated') # test reactivating the user isn't enough to authenticate, as the native source # refuse to authenticate user from other sources os.system(deletecmd) diff -r 97202ea671e4 -r 310040c668c0 sobjects/ldapparser.py --- a/sobjects/ldapparser.py Wed Jan 09 15:46:05 2013 +0100 +++ b/sobjects/ldapparser.py Wed Jan 09 16:06:20 2013 +0100 @@ -22,7 +22,7 @@ """ from __future__ import with_statement -from logilab.common.decorators import cached +from logilab.common.decorators import cached, cachedproperty from logilab.common.shellutils import generate_password from cubicweb import Binary, ConfigurationError @@ -36,15 +36,27 @@ # attributes of the cw user non_attribute_keys = set(('email',)) + @cachedproperty + def searchfilterstr(self): + """ ldap search string, including user-filter """ + return '(&%s)' % ''.join(self.source.base_filters) + + @cachedproperty + def source_entities_by_extid(self): + source = self.source + return dict((userdict['dn'], userdict) + for userdict in source._search(self._cw, + source.user_base_dn, + source.user_base_scope, + self.searchfilterstr)) + def process(self, url, raise_on_error=False): """IDataFeedParser main entry point""" - source = self.source - searchstr = '(&%s)' % ''.join(source.base_filters) - self.warning('processing ldapfeed stuff %s %s', source, searchstr) - for userdict in source._search(self._cw, source.user_base_dn, - source.user_base_scope, searchstr): + self.debug('processing ldapfeed source %s %s', self.source, self.searchfilterstr) + for userdict in self.source_entities_by_extid.itervalues(): self.warning('fetched user %s', userdict) - entity = self.extid2entity(userdict['dn'], 'CWUser', **userdict) + extid = userdict['dn'] + entity = self.extid2entity(extid, 'CWUser', **userdict) if entity is not None and not self.created_during_pull(entity): self.notify_updated(entity) attrs = self.ldap2cwattrs(userdict) @@ -78,7 +90,8 @@ if entity.__regid__ == 'CWUser': wf = entity.cw_adapt_to('IWorkflowable') if wf.state == 'deactivated': - self.warning('update on deactivated user %s', entity.login) + wf.fire_transition('activate') + self.warning('user %s reactivated', entity.login) mdate = attrs.get('modification_date') if not mdate or mdate > entity.modification_date: attrs = dict( (k, v) for k, v in attrs.iteritems() @@ -121,12 +134,14 @@ entity.cw_set(in_group=groups) self._process_email(entity, sourceparams) - def is_deleted(self, extid, etype, eid): + def is_deleted(self, extidplus, etype, eid): try: - extid, _ = extid.rsplit('@@', 1) + extid, _ = extidplus.rsplit('@@', 1) except ValueError: - pass - return not self.source.object_exists_in_ldap(extid) + # for some reason extids here tend to come in both forms, e.g: + # dn, dn@@Babar + extid = extidplus + return extid not in self.source_entities_by_extid def _process_email(self, entity, userdict): try: diff -r 97202ea671e4 -r 310040c668c0 web/request.py --- a/web/request.py Wed Jan 09 15:46:05 2013 +0100 +++ b/web/request.py Wed Jan 09 16:06:20 2013 +0100 @@ -600,24 +600,35 @@ name = bwcompat self.set_cookie(name, '', maxage=0, expires=date(2000, 1, 1)) - def set_content_type(self, content_type, filename=None, encoding=None): + def set_content_type(self, content_type, filename=None, encoding=None, + disposition='inline'): """set output content type for this request. An optional filename - may be given + may be given. + + The disposition argument may be `attachement` or `inline` as specified + for the Content-disposition HTTP header. The disposition parameter have + no effect if no filename are specified. """ if content_type.startswith('text/') and ';charset=' not in content_type: content_type += ';charset=' + (encoding or self.encoding) self.set_header('content-type', content_type) if filename: - header = ['attachment'] + header = [disposition] + unicode_filename = None try: - filename = filename.encode('ascii') - header.append('filename=' + filename) + ascii_filename = filename.encode('ascii') except UnicodeEncodeError: # fallback filename for very old browser - header.append('filename=' + filename.encode('ascii', 'ignore')) + unicode_filename = filename + ascii_filename = filename.encode('ascii', 'ignore') + # escape " and \ + # see http://greenbytes.de/tech/tc2231/#attwithfilenameandextparamescaped + ascii_filename = ascii_filename.replace('\x5c', r'\\').replace('"', r'\"') + header.append('filename="%s"' % ascii_filename) + if unicode_filename is not None: # encoded filename according RFC5987 - filename = urllib.quote(filename.encode('utf-8'), '') - header.append("filename*=utf-8''" + filename) + urlquoted_filename = urllib.quote(unicode_filename.encode('utf-8'), '') + header.append("filename*=utf-8''" + urlquoted_filename) self.set_header('content-disposition', ';'.join(header)) # high level methods for HTML headers management ########################## diff -r 97202ea671e4 -r 310040c668c0 web/test/unittest_idownloadable.py --- a/web/test/unittest_idownloadable.py Wed Jan 09 15:46:05 2013 +0100 +++ b/web/test/unittest_idownloadable.py Wed Jan 09 16:06:20 2013 +0100 @@ -58,12 +58,44 @@ req.form['eid'] = str(req.user.eid) data = self.ctrl_publish(req,'view') get = req.headers_out.getRawHeaders - self.assertEqual(['attachment;filename=admin.txt'], + self.assertEqual(['attachment;filename="admin.txt"'], get('content-disposition')) self.assertEqual(['text/plain;charset=ascii'], get('content-type')) self.assertEqual('Babar is not dead!', data) + def test_header_with_space(self): + req = self.request() + self.create_user(req, login=u'c c l a', password='babar') + self.commit() + with self.login(u'c c l a', password='babar'): + req = self.request() + req.form['vid'] = 'download' + req.form['eid'] = str(req.user.eid) + data = self.ctrl_publish(req,'view') + get = req.headers_out.getRawHeaders + self.assertEqual(['attachment;filename="c c l a.txt"'], + get('content-disposition')) + self.assertEqual(['text/plain;charset=ascii'], + get('content-type')) + self.assertEqual('Babar is not dead!', data) + + def test_header_with_space_and_comma(self): + req = self.request() + self.create_user(req, login=ur'c " l\ a', password='babar') + self.commit() + with self.login(ur'c " l\ a', password='babar'): + req = self.request() + req.form['vid'] = 'download' + req.form['eid'] = str(req.user.eid) + data = self.ctrl_publish(req,'view') + get = req.headers_out.getRawHeaders + self.assertEqual([r'attachment;filename="c \" l\\ a.txt"'], + get('content-disposition')) + self.assertEqual(['text/plain;charset=ascii'], + get('content-type')) + self.assertEqual('Babar is not dead!', data) + def test_header_unicode_filename(self): req = self.request() self.create_user(req, login=u'cécilia', password='babar') @@ -74,7 +106,7 @@ req.form['eid'] = str(req.user.eid) self.ctrl_publish(req,'view') get = req.headers_out.getRawHeaders - self.assertEqual(["attachment;filename=ccilia.txt;filename*=utf-8''c%C3%A9cilia.txt"], + self.assertEqual(['''attachment;filename="ccilia.txt";filename*=utf-8''c%C3%A9cilia.txt'''], get('content-disposition')) def test_header_unicode_long_filename(self): @@ -88,7 +120,7 @@ req.form['eid'] = str(req.user.eid) self.ctrl_publish(req,'view') get = req.headers_out.getRawHeaders - 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"], + 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')) if __name__ == '__main__': diff -r 97202ea671e4 -r 310040c668c0 web/test/unittest_views_json.py --- a/web/test/unittest_views_json.py Wed Jan 09 15:46:05 2013 +0100 +++ b/web/test/unittest_views_json.py Wed Jan 09 16:06:20 2013 +0100 @@ -37,6 +37,13 @@ self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/json']) self.assertEqual(data, '[["guests", 1], ["managers", 1]]') + def test_json_rsetexport_empty_rset(self): + req = self.request() + rset = req.execute('Any X WHERE X is CWUser, X login "foobarbaz"') + data = self.view('jsonexport', rset) + self.assertEqual(req.headers_out.getRawHeaders('content-type'), ['application/json']) + self.assertEqual(data, '[]') + def test_json_rsetexport_with_jsonp(self): req = self.request() req.form.update({'callback': 'foo', diff -r 97202ea671e4 -r 310040c668c0 web/test/unittest_viewselector.py --- a/web/test/unittest_viewselector.py Wed Jan 09 15:46:05 2013 +0100 +++ b/web/test/unittest_viewselector.py Wed Jan 09 16:06:20 2013 +0100 @@ -111,8 +111,8 @@ def test_possible_views_noresult(self): req = self.request() rset = req.execute('Any X WHERE X eid 999999') - self.assertListEqual(self.pviews(req, rset), - []) + self.assertListEqual([('jsonexport', json.JsonRsetView)], + self.pviews(req, rset)) def test_possible_views_one_egroup(self): req = self.request() diff -r 97202ea671e4 -r 310040c668c0 web/views/idownloadable.py --- a/web/views/idownloadable.py Wed Jan 09 15:46:05 2013 +0100 +++ b/web/views/idownloadable.py Wed Jan 09 16:06:20 2013 +0100 @@ -100,7 +100,8 @@ contenttype = adapter.download_content_type() self._cw.set_content_type(contenttype or self.content_type, filename=adapter.download_file_name(), - encoding=encoding) + encoding=encoding, + disposition='attachment') def call(self): entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0) diff -r 97202ea671e4 -r 310040c668c0 web/views/json.py --- a/web/views/json.py Wed Jan 09 15:46:05 2013 +0100 +++ b/web/views/json.py Wed Jan 09 16:06:20 2013 +0100 @@ -23,6 +23,7 @@ _ = unicode from cubicweb.utils import json_dumps +from cubicweb.predicates import any_rset from cubicweb.view import EntityView, AnyRsetView from cubicweb.web.application import anonymized_request from cubicweb.web.views import basecontrollers @@ -90,6 +91,7 @@ class JsonRsetView(JsonMixIn, AnyRsetView): """dumps raw result set in JSON format""" __regid__ = 'jsonexport' + __select__ = any_rset() # means rset might be empty or have any shape title = _('json-export-view') def call(self):