--- 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
--- 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"
--- 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 <aurelien.campeas@logilab.fr> Wed, 09 Jan 2013 15:40:00 +0100
+
+cubicweb (3.15.7-1) squeeze; urgency=low
+
+ * New upstream release
+
+ -- David Douard <david.douard@logilab.fr> Wed, 12 Dec 2012 22:10:45 +0100
+
cubicweb (3.15.6-1) squeeze; urgency=low
* New upstream release
--- 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)
--- 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.
--- 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 |
--- 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)
--- 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 = {}
--- 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,
--- 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:
--- 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))
--- 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']:
--- 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)
--- 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:
--- 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 ##########################
--- 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__':
--- 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',
--- 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()
--- 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)
--- 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):