Repository: ckan/ckan

Issue:
Remove legacy Search API
### CKAN Version if known (or site URL)

Can we officially deprecate the legacy [Search API and Utils APIs](https://docs.ckan.org/en/latest/api/legacy-api.html)?

These was described as "legacy API"s since CKAN 2.3 (here)[https://github.com/ckan/ckan/commit/254fca2d43ccacbe674df2da42aba83897dcdb96#diff-b4e5718590c95267775a87dafc922c76). However they have not been marked as deprecated in the changelog.

These are the APIs:
* `/api/search/dataset` / `/api/search/package` (replaced by `package_search`)
* `/api/search/resource` (replaced by `resource_search`)
* `/api/search/revision` (revisions are deprecated)
* `/api/tag_counts` (can be done with `package_search facet.field='tags'`)
* `/api/util/dataset/autocomplete` (replaced by `package_autocomplete`)
* `/api/util/tag/autocomplete` (replaced by `tag_autocomplete`)
* `/api/util/resource/format_autocomplete` (replaced by `format_autocomplete`)
* `/api/util/dataset/munge_name`
* `/api/util/dataset/munge_title_to_name`
* `/api/util/tag/munge`

I can't see any replacements for the munge functions and I suspect they are still useful to anyone replacing the CKAN front-end. Should we turn these into action funcs?

(We [got rid of the REST API `/api/rest/*` in 2.8](https://github.com/ckan/ckan/pull/4069), having been marked as deprecated in the 2.7 changelog)



Discussion:
That sounds very sensible.

I think that that the munge functions can be easily turned into actions as you suggest, and the rest we should just remove.

Resolving patch:
diff --git a/ckan/config/routing.py b/ckan/config/routing.py
--- a/ckan/config/routing.py
+++ b/ckan/config/routing.py
@@ -112,19 +112,6 @@ def make_map():
         if not hasattr(route, '_ckan_core'):
             route._ckan_core = False
 
-    # CKAN API versioned.
-    register_list = [
-        'package', 'dataset', 'resource', 'group', 'revision',
-        'licenses', 'rating', 'user', 'activity'
-    ]
-    register_list_str = '|'.join(register_list)
-
-    # /api ver 1, 2, 3 or none
-    with SubMapper(
-            map, controller='api', path_prefix='/api{ver:/1|/2|/3|}',
-            ver='/1') as m:
-        m.connect('/search/{register}', action='search')
-
     # /api/util ver 1, 2 or none
     with SubMapper(
             map, controller='api', path_prefix='/api{ver:/1|/2|}',
diff --git a/ckan/controllers/api.py b/ckan/controllers/api.py
--- a/ckan/controllers/api.py
+++ b/ckan/controllers/api.py
@@ -257,143 +257,6 @@ def action(self, logic_function, ver=None):
             return self._finish(500, return_dict, content_type='json')
         return self._finish_ok(return_dict)
 
-    def _get_action_from_map(self, action_map, register, subregister):
-        ''' Helper function to get the action function specified in
-            the action map'''
-
-        # translate old package calls to use dataset
-        if register == 'package':
-            register = 'dataset'
-
-        action = action_map.get((register, subregister))
-        if not action:
-            action = action_map.get(register)
-        if action:
-            return get_action(action)
-
-    def search(self, ver=None, register=None):
-
-        log.debug('search %s params: %r', register, request.params)
-        if register == 'revision':
-            since_time = None
-            if 'since_id' in request.params:
-                id = request.params['since_id']
-                if not id:
-                    return self._finish_bad_request(
-                        _(u'No revision specified'))
-                rev = model.Session.query(model.Revision).get(id)
-                if rev is None:
-                    return self._finish_not_found(
-                        _(u'There is no revision with id: %s') % id)
-                since_time = rev.timestamp
-            elif 'since_time' in request.params:
-                since_time_str = request.params['since_time']
-                try:
-                    since_time = h.date_str_to_datetime(since_time_str)
-                except ValueError as inst:
-                    return self._finish_bad_request('ValueError: %s' % inst)
-            else:
-                return self._finish_bad_request(
-                    _("Missing search term ('since_id=UUID' or " +
-                      " 'since_time=TIMESTAMP')"))
-            revs = model.Session.query(model.Revision) \
-                .filter(model.Revision.timestamp > since_time) \
-                .order_by(model.Revision.timestamp) \
-                .limit(50)  # reasonable enough for a page
-            return self._finish_ok([rev.id for rev in revs])
-        elif register in ['dataset', 'package', 'resource']:
-            try:
-                params = MultiDict(self._get_search_params(request.params))
-            except ValueError as e:
-                return self._finish_bad_request(
-                    _('Could not read parameters: %r' % e))
-
-            # if using API v2, default to returning the package ID if
-            # no field list is specified
-            if register in ['dataset', 'package'] and not params.get('fl'):
-                params['fl'] = 'id' if ver == 2 else 'name'
-
-            try:
-                if register == 'resource':
-                    query = search.query_for(model.Resource)
-
-                    # resource search still uses ckan query parser
-                    options = search.QueryOptions()
-                    for k, v in params.items():
-                        if (k in search.DEFAULT_OPTIONS.keys()):
-                            options[k] = v
-                    options.update(params)
-                    options.username = c.user
-                    options.search_tags = False
-                    options.return_objects = False
-                    query_fields = MultiDict()
-                    for field, value in params.items():
-                        field = field.strip()
-                        if field in search.DEFAULT_OPTIONS.keys() or \
-                                field in IGNORE_FIELDS:
-                            continue
-                        values = [value]
-                        if isinstance(value, list):
-                            values = value
-                        for v in values:
-                            query_fields.add(field, v)
-
-                    results = query.run(
-                        query=params.get('q'),
-                        fields=query_fields,
-                        options=options
-                    )
-                else:
-                    # For package searches in API v3 and higher, we can pass
-                    # parameters straight to Solr.
-                    if ver in [1, 2]:
-                        # Otherwise, put all unrecognised ones into the q
-                        # parameter
-                        params = search.\
-                            convert_legacy_parameters_to_solr(params)
-                    query = search.query_for(model.Package)
-
-                    # Remove any existing fq param and set the capacity to
-                    # public
-                    if 'fq' in params:
-                        del params['fq']
-                    params['fq'] = '+capacity:public'
-                    # if callback is specified we do not want to send that to
-                    # the search
-                    if 'callback' in params:
-                        del params['callback']
-                    results = query.run(params)
-                return self._finish_ok(results)
-            except search.SearchError as e:
-                log.exception(e)
-                return self._finish_bad_request(
-                    _('Bad search option: %s') % e)
-        else:
-            return self._finish_not_found(
-                _('Unknown register: %s') % register)
-
-    @classmethod
-    def _get_search_params(cls, request_params):
-        if 'qjson' in request_params:
-            try:
-                qjson_param = request_params['qjson'].replace('\\\\u', '\\u')
-                params = h.json.loads(qjson_param, encoding='utf8')
-            except ValueError as e:
-                raise ValueError(_('Malformed qjson value: %r')
-                                 % e)
-        elif len(request_params) == 1 and \
-            len(request_params.values()[0]) < 2 and \
-                request_params.keys()[0].startswith('{'):
-            # e.g. {some-json}='1' or {some-json}=''
-            params = h.json.loads(request_params.keys()[0], encoding='utf8')
-        else:
-            params = request_params
-        if not isinstance(params, (UnicodeMultiDict, dict)):
-            msg = _('Request params must be in form ' +
-                    'of a json encoded dictionary.')
-            raise ValueError(msg)
-        return params
-
     @jsonp.jsonpify
     def user_autocomplete(self):
         q = request.params.get('q', '')
@@ -444,59 +307,6 @@ def organization_autocomplete(self):
                 get_action('organization_autocomplete')(context, data_dict)
         return organization_list
 
-    def dataset_autocomplete(self):
-        q = request.params.get('incomplete', '')
-        limit = request.params.get('limit', 10)
-        package_dicts = []
-        if q:
-            context = {'model': model, 'session': model.Session,
-                       'user': c.user, 'auth_user_obj': c.userobj}
-
-            data_dict = {'q': q, 'limit': limit}
-
-            package_dicts = get_action('package_autocomplete')(context,
-                                                               data_dict)
-
-        resultSet = {'ResultSet': {'Result': package_dicts}}
-        return self._finish_ok(resultSet)
-
-    def tag_autocomplete(self):
-        q = request.str_params.get('incomplete', '')
-        q = text_type(urllib.unquote(q), 'utf-8')
-        limit = request.params.get('limit', 10)
-        tag_names = []
-        if q:
-            context = {'model': model, 'session': model.Session,
-                       'user': c.user, 'auth_user_obj': c.userobj}
-
-            data_dict = {'q': q, 'limit': limit}
-
-            tag_names = get_action('tag_autocomplete')(context, data_dict)
-
-        resultSet = {
-            'ResultSet': {
-                'Result': [{'Name': tag} for tag in tag_names]
-            }
-        }
-        return self._finish_ok(resultSet)
-
-    def format_autocomplete(self):
-        q = request.params.get('incomplete', '')
-        limit = request.params.get('limit', 5)
-        formats = []
-        if q:
-            context = {'model': model, 'session': model.Session,
-                       'user': c.user, 'auth_user_obj': c.userobj}
-            data_dict = {'q': q, 'limit': limit}
-            formats = get_action('format_autocomplete')(context, data_dict)
-
-        resultSet = {
-            'ResultSet': {
-                'Result': [{'Format': format} for format in formats]
-            }
-        }
-        return self._finish_ok(resultSet)
-
     def munge_package_name(self):
         name = request.params.get('name')
         munged_name = munge.munge_name(name)
diff --git a/ckan/logic/auth/create.py b/ckan/logic/auth/create.py
--- a/ckan/logic/auth/create.py
+++ b/ckan/logic/auth/create.py
@@ -149,10 +149,12 @@ def user_create(context, data_dict=None):
             'create users')}
     return {'success': True}
 
+
 def user_invite(context, data_dict):
     data_dict['id'] = data_dict['group_id']
     return group_member_create(context, data_dict)
 
+
 def _check_group_auth(context, data_dict):
     '''Has this user got update permission for all of the given groups?
     If there is a package in the context then ignore that package's groups.
@@ -205,14 +207,17 @@ def vocabulary_create(context, data_dict):
     # sysadmins only
     return {'success': False}
 
+
 def activity_create(context, data_dict):
     # sysadmins only
     return {'success': False}
 
+
 def tag_create(context, data_dict):
     # sysadmins only
     return {'success': False}
 
+
 def _group_or_org_member_create(context, data_dict):
     user = context['user']
     group_id = data_dict['id']
@@ -220,12 +225,15 @@ def _group_or_org_member_create(context, data_dict):
         return {'success': False, 'msg': _('User %s not authorized to add members') % user}
     return {'success': True}
 
+
 def organization_member_create(context, data_dict):
     return _group_or_org_member_create(context, data_dict)
 
+
 def group_member_create(context, data_dict):
     return _group_or_org_member_create(context, data_dict)
 
+
 def member_create(context, data_dict):
     group = logic_auth.get_group_object(context, data_dict)
     user = context['user']


Test patch:
diff --git a/ckan/tests/controllers/test_api.py b/ckan/tests/controllers/test_api.py
--- a/ckan/tests/controllers/test_api.py
+++ b/ckan/tests/controllers/test_api.py
@@ -314,87 +314,3 @@ def test_jsonp_does_not_work_on_post_requests(self):
         eq_(res_dict['success'], True)
         eq_(sorted(res_dict['result']),
             sorted([dataset1['name'], dataset2['name']]))
-
-
-class TestRevisionSearch(helpers.FunctionalTestBase):
-
-    # Error cases
-
-    def test_no_search_term(self):
-        app = self._get_test_app()
-        response = app.get('/api/search/revision', status=400)
-        assert_in('Bad request - Missing search term', response.body)
-
-    def test_no_search_term_api_v2(self):
-        app = self._get_test_app()
-        response = app.get('/api/2/search/revision', status=400)
-        assert_in('Bad request - Missing search term', response.body)
-
-    def test_date_instead_of_revision(self):
-        app = self._get_test_app()
-        response = app.get('/api/search/revision'
-                           '?since_id=2010-01-01T00:00:00', status=404)
-        assert_in('Not found - There is no revision', response.body)
-
-    def test_date_invalid(self):
-        app = self._get_test_app()
-        response = app.get('/api/search/revision'
-                           '?since_time=2010-02-31T00:00:00', status=400)
-        assert_in('Bad request - ValueError: day is out of range for month',
-                  response.body)
-
-    def test_no_value(self):
-        app = self._get_test_app()
-        response = app.get('/api/search/revision?since_id=', status=400)
-        assert_in('Bad request - No revision specified', response.body)
-
-    def test_revision_doesnt_exist(self):
-        app = self._get_test_app()
-        response = app.get('/api/search/revision?since_id=1234', status=404)
-        assert_in('Not found - There is no revision', response.body)
-
-    def test_revision_doesnt_exist_api_v2(self):
-        app = self._get_test_app()
-        response = app.get('/api/2/search/revision?since_id=1234', status=404)
-        assert_in('Not found - There is no revision', response.body)
-
-    # Normal usage
-
-    @classmethod
-    def _create_revisions(cls, num_revisions):
-        rev_ids = []
-        for i in xrange(num_revisions):
-            rev = model.repo.new_revision()
-            rev.id = text_type(i)
-            model.Session.commit()
-            rev_ids.append(rev.id)
-        return rev_ids
-
-    def test_revision_since_id(self):
-        rev_ids = self._create_revisions(4)
-        app = self._get_test_app()
-
-        response = app.get('/api/2/search/revision?since_id=%s' % rev_ids[1])
-
-        res = json.loads(response.body)
-        assert_equal(res, rev_ids[2:])
-
-    def test_revision_since_time(self):
-        rev_ids = self._create_revisions(4)
-        app = self._get_test_app()
-
-        rev1 = model.Session.query(model.Revision).get(rev_ids[1])
-        response = app.get('/api/2/search/revision?since_time=%s'
-                           % rev1.timestamp.isoformat())
-
-        res = json.loads(response.body)
-        assert_equal(res, rev_ids[2:])
-
-    def test_revisions_returned_are_limited(self):
-        rev_ids = self._create_revisions(55)
-        app = self._get_test_app()
-
-        response = app.get('/api/2/search/revision?since_id=%s' % rev_ids[1])
-
-        res = json.loads(response.body)
-        assert_equal(res, rev_ids[2:52])  # i.e. limited to 50
diff --git a/ckan/tests/legacy/functional/api/test_package_search.py b/ckan/tests/legacy/functional/api/test_package_search.py
--- a/ckan/tests/legacy/functional/api/test_package_search.py
+++ b/ckan/tests/legacy/functional/api/test_package_search.py
@@ -1,17 +1,12 @@
 # encoding: utf-8
 
-from nose.tools import assert_raises
-from nose.plugins.skip import SkipTest
-
 from urllib import quote
 
-from ckan import plugins
 import ckan.lib.search as search
 from ckan.tests.legacy import setup_test_search_index
 from ckan.tests.legacy.functional.api.base import *
 from ckan.tests.legacy import TestController as ControllerTestCase
-from ckan.controllers.api import ApiController
-from webob.multidict import UnicodeMultiDict
+
 
 class PackageSearchApiTestCase(ApiTestCase, ControllerTestCase):
 
@@ -31,7 +26,7 @@ def setup_class(self):
                        'geographic_coverage':'England, Wales'},
         }
         CreateTestData.create_arbitrary(self.package_fixture_data)
-        self.base_url = self.offset('/search/dataset')
+        self.base_url = self.offset('/action/package_search')
 
     @classmethod
     def teardown_class(cls):
@@ -39,60 +34,23 @@ def teardown_class(cls):
         search.clear_all()
 
     def assert_results(self, res_dict, expected_package_names):
-        expected_pkgs = [self.package_ref_from_name(expected_package_name) \
-                         for expected_package_name in expected_package_names]
-        assert_equal(set(res_dict['results']), set(expected_pkgs))
-
-    def test_00_read_search_params(self):
-        def check(request_params, expected_params):
-            params = ApiController._get_search_params(request_params)
-            assert_equal(params, expected_params)
-        # uri parameters
-        check(UnicodeMultiDict({'q': '', 'ref': 'boris'}),
-              {"q": "", "ref": "boris"})
-        # uri json
-        check(UnicodeMultiDict({'qjson': '{"q": "", "ref": "boris"}'}),
-              {"q": "", "ref": "boris"})
-        # posted json
-        check(UnicodeMultiDict({'{"q": "", "ref": "boris"}': u'1'}),
-              {"q": "", "ref": "boris"})
-        check(UnicodeMultiDict({'{"q": "", "ref": "boris"}': u''}),
-              {"q": "", "ref": "boris"})
-        # no parameters
-        check(UnicodeMultiDict({}),
-              {})
-
-    def test_00_read_search_params_with_errors(self):
-        def check_error(request_params):
-            assert_raises(ValueError, ApiController._get_search_params, request_params)
-        # uri json
-        check_error(UnicodeMultiDict({'qjson': '{"q": illegal json}'}))
-        # posted json
-        check_error(UnicodeMultiDict({'{"q": illegal json}': u'1'}))
+        result = res_dict['result']['results'][0]
+        assert_equal(result['name'], expected_package_names)
 
     def test_01_uri_q(self):
         offset = self.base_url + '?q=%s' % self.package_fixture_data['name']
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, ['testpkg'])
-        assert res_dict['count'] == 1, res_dict['count']
+        self.assert_results(res_dict, 'testpkg')
+        assert res_dict['result']['count'] == 1, res_dict['result']['count']
 
     def test_02_post_q(self):
         offset = self.base_url
         query = {'q':'testpkg'}
         res = self.app.post(offset, params=query, status=200)
         res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, ['testpkg'])
-        assert res_dict['count'] == 1, res_dict['count']
-
-    def test_03_uri_qjson(self):
-        query = {'q': self.package_fixture_data['name']}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, ['testpkg'])
-        assert res_dict['count'] == 1, res_dict['count']
+        self.assert_results(res_dict, 'testpkg')
+        assert res_dict['result']['count'] == 1, res_dict['result']['count']
 
     def test_04_post_json(self):
         query = {'q': self.package_fixture_data['name']}
@@ -100,71 +58,40 @@ def test_04_post_json(self):
         offset = self.base_url
         res = self.app.post(offset, params=json_query, status=200)
         res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, ['testpkg'])
-        assert res_dict['count'] == 1, res_dict['count']
-
-    def test_05_uri_json_tags(self):
-        query = {'q': 'annakarenina tags:russian tags:tolstoy'}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina'])
-        assert res_dict['count'] == 1, res_dict
-
-    def test_05_uri_json_tags_multiple(self):
-        query = {'q': 'tags:russian tags:tolstoy'}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina'])
-        assert res_dict['count'] == 1, res_dict
+        self.assert_results(res_dict, 'testpkg')
+        assert res_dict['result']['count'] == 1, res_dict['result']['count']
 
     def test_06_uri_q_tags(self):
         query = webhelpers.util.html_escape('annakarenina tags:russian tags:tolstoy')
         offset = self.base_url + '?q=%s' % query
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina'])
-        assert res_dict['count'] == 1, res_dict['count']
-
-    def test_08_uri_qjson_malformed(self):
-        offset = self.base_url + '?qjson="q":""' # user forgot the curly braces
-        res = self.app.get(offset, status=400)
-        self.assert_json_response(res, 'Bad request - Could not read parameters')
+        self.assert_results(res_dict, 'annakarenina')
+        assert res_dict['result']['count'] == 1, res_dict['count']
 
     def test_09_just_tags(self):
         offset = self.base_url + '?q=tags:russian'
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 2, res_dict
+        assert res_dict['result']['count'] == 2, res_dict
 
     def test_10_multiple_tags(self):
         offset = self.base_url + '?q=tags:tolstoy tags:russian'
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 1, res_dict
-
-    def test_12_all_packages_qjson(self):
-        query = {'q': ''}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        assert_equal(res_dict['count'], 3)
+        assert res_dict['result']['count'] == 1, res_dict
 
     def test_12_all_packages_q(self):
         offset = self.base_url + '?q=""'
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        assert_equal(res_dict['count'], 3)
+        assert_equal(res_dict['result']['count'], 3)
 
     def test_12_all_packages_no_q(self):
         offset = self.base_url
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        assert_equal(res_dict['count'], 3)
+        assert_equal(res_dict['result']['count'], 3)
 
     def test_12_filter_by_openness(self):
         offset = self.base_url + '?filter_by_openness=1'
@@ -203,95 +130,6 @@ def teardown_class(cls):
         model.repo.rebuild_db()
         search.clear_all()
 
-    def test_07_uri_qjson_tags(self):
-        query = {'q': '', 'tags':['tolstoy']}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina'])
-        assert res_dict['count'] == 1, res_dict
-
-    def test_07_uri_qjson_tags_with_flexible_query(self):
-        query = {'q': '', 'tags':['Flexible \u30a1']}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina', u'warandpeace'])
-        assert res_dict['count'] == 2, res_dict
-
-    def test_07_uri_qjson_tags_multiple(self):
-        query = {'q': '', 'tags':['tolstoy', 'russian', u'Flexible \u30a1']}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        print(offset)
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina'])
-        assert res_dict['count'] == 1, res_dict
-
-    def test_07_uri_qjson_tags_reverse(self):
-        query = {'q': '', 'tags':['russian']}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina', u'warandpeace'])
-        assert res_dict['count'] == 2, res_dict
-
-    def test_07_uri_qjson_extras(self):
-        # TODO: solr is not currently set up to allow partial matches
-        #       and extras are not saved as multivalued so this
-        #       test will fail. Make extras multivalued or remove?
-        raise SkipTest()
-
-        query = {"geographic_coverage":"England"}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, ['testpkg'])
-        assert res_dict['count'] == 1, res_dict
-
-    def test_07_uri_qjson_extras_2(self):
-        query = {"national_statistic":"yes"}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, ['testpkg'])
-        assert res_dict['count'] == 1, res_dict
-
-    def test_08_all_fields(self):
-        rating = model.Rating(user_ip_address=u'123.1.2.3',
-                              package=self.anna,
-                              rating=3.0)
-        model.Session.add(rating)
-        model.repo.commit_and_remove()
-
-        query = {'q': 'russian', 'all_fields': 1}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 2, res_dict
-        for rec in res_dict['results']:
-            if rec['name'] == 'annakarenina':
-                anna_rec = rec
-                break
-        assert anna_rec['name'] == 'annakarenina', res_dict['results']
-        assert anna_rec['title'] == 'A Novel By Tolstoy', anna_rec['title']
-        assert anna_rec['license_id'] == u'other-open', anna_rec['license_id']
-        assert len(anna_rec['tags']) == 3, anna_rec['tags']
-        for expected_tag in ['russian', 'tolstoy', u'Flexible \u30a1']:
-            assert expected_tag in anna_rec['tags'], anna_rec['tags']
-
-        # try alternative syntax
-        offset = self.base_url + '?q=russian&all_fields=1'
-        res2 = self.app.get(offset, status=200)
-        assert_equal(res2.body, res.body)
-
     def test_08_all_fields_syntax_error(self):
         offset = self.base_url + '?all_fields=should_be_boolean' # invalid all_fields value
         res = self.app.get(offset, status=400)
@@ -331,139 +169,47 @@ def test_10_many_tags_with_ampersand(self):
         res_dict = self.data_from_res(res)
         assert res_dict['count'] == 1, res_dict
 
-    def test_11_pagination_limit(self):
-        offset = self.base_url + '?all_fields=1&q=tags:russian&limit=1&order_by=name'
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 2, res_dict
-        assert len(res_dict['results']) == 1, res_dict
-        assert res_dict['results'][0]['name'] == 'annakarenina', res_dict['results'][0]['name']
-
-    def test_11_pagination_offset_limit(self):
-        offset = self.base_url + '?all_fields=1&q=tags:russian&offset=1&limit=1&order_by=name'
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 2, res_dict
-        assert len(res_dict['results']) == 1, res_dict
-        assert res_dict['results'][0]['name'] == 'warandpeace', res_dict['results'][0]['name']
-
-    def test_11_pagination_syntax_error(self):
-        offset = self.base_url + '?all_fields=1&q="tags:russian"&start=should_be_integer&rows=1&order_by=name' # invalid offset value
-        res = self.app.get(offset, status=400)
-        print(res.body)
-        assert('should_be_integer' in res.body)
-
     def test_13_just_groups(self):
         offset = self.base_url + '?groups=roger'
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 1, res_dict
+        assert res_dict['result']['count'] == 1, res_dict
 
     def test_14_empty_parameter_ignored(self):
         offset = self.base_url + '?groups=roger&title='
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 1, res_dict
+        assert res_dict['result']['count'] == 1, res_dict
 
 
 class TestPackageSearchApi3(Api3TestCase, PackageSearchApiTestCase):
     '''Here are tests with URIs in specifically SOLR syntax.'''
-    def test_07_uri_qjson_tags(self):
-        query = {'q': 'tags:tolstoy'}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina'])
-        assert res_dict['count'] == 1, res_dict
-
-    def test_07_uri_qjson_tags_with_unicode(self):
-        query = {'q': u'tags:"Flexible \u30a1"'}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina', u'warandpeace'])
-        assert res_dict['count'] == 2, res_dict
-
-    def test_07_uri_qjson_tags_multiple(self):
-        query = {'q': 'tags:tolstoy tags:russian'}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        print(offset)
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina'])
-        assert res_dict['count'] == 1, res_dict
-
-    def test_07_uri_qjson_tags_reverse(self):
-        query = {'q': 'tags:russian'}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, [u'annakarenina', u'warandpeace'])
-        assert res_dict['count'] == 2, res_dict
-
-    def test_07_uri_qjson_extras_2(self):
-        query = {'q': "national_statistic:yes"}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        self.assert_results(res_dict, ['testpkg'])
-        assert res_dict['count'] == 1, res_dict
-
-    def test_08_all_fields(self):
-        query = {'q': 'russian', 'fl': '*'}
-        json_query = self.dumps(query)
-        offset = self.base_url + '?qjson=%s' % json_query
-        res = self.app.get(offset, status=200)
-        res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 2, res_dict
-        for rec in res_dict['results']:
-            if rec['name'] == 'annakarenina':
-                anna_rec = rec
-                break
-        assert anna_rec['name'] == 'annakarenina', res_dict['results']
-        assert anna_rec['title'] == 'A Novel By Tolstoy', anna_rec['title']
-        assert anna_rec['license_id'] == u'other-open', anna_rec['license_id']
-        assert len(anna_rec['tags']) == 3, anna_rec['tags']
-        for expected_tag in ['russian', 'tolstoy', u'Flexible \u30a1']:
-            assert expected_tag in anna_rec['tags']
-
-        # try alternative syntax
-        offset = self.base_url + '?q=russian&fl=*'
-        res2 = self.app.get(offset, status=200)
-        assert_equal(res2.body, res.body)
-
     def test_09_just_tags(self):
         offset = self.base_url + '?q=tags:russian&fl=*'
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 2, res_dict
+        assert res_dict['result']['count'] == 2, res_dict
 
     def test_11_pagination_limit(self):
         offset = self.base_url + '?fl=*&q=tags:russian&rows=1&sort=name asc'
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 2, res_dict
-        assert len(res_dict['results']) == 1, res_dict
-        assert res_dict['results'][0]['name'] == 'annakarenina', res_dict['results'][0]['name']
+        assert res_dict['result']['count'] == 2, res_dict
+        assert len(res_dict['result']['results']) == 1, res_dict
+        self.assert_results(res_dict, 'annakarenina')
 
     def test_11_pagination_offset_limit(self):
         offset = self.base_url + '?fl=*&q=tags:russian&start=1&rows=1&sort=name asc'
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 2, res_dict
-        assert len(res_dict['results']) == 1, res_dict
-        assert res_dict['results'][0]['name'] == 'warandpeace', res_dict['results'][0]['name']
+        assert res_dict['result']['count'] == 2, res_dict
+        assert len(res_dict['result']['results']) == 1, res_dict
+        self.assert_results(res_dict, 'warandpeace')
 
-    def test_11_pagination_syntax_error(self):
+    def test_11_pagination_validation_error(self):
         offset = self.base_url + '?fl=*&q=tags:russian&start=should_be_integer&rows=1&sort=name asc' # invalid offset value
-        res = self.app.get(offset, status=400)
-        print(res.body)
-        assert('should_be_integer' in res.body)
+        res = self.app.get(offset, status=409)
+        assert('Validation Error' in res.body)
 
     def test_12_v1_or_v2_syntax(self):
         offset = self.base_url + '?all_fields=1'
@@ -474,4 +220,4 @@ def test_13_just_groups(self):
         offset = self.base_url + '?q=groups:roger'
         res = self.app.get(offset, status=200)
         res_dict = self.data_from_res(res)
-        assert res_dict['count'] == 1, res_dict
+        assert res_dict['result']['count'] == 1, res_dict
diff --git a/ckan/tests/legacy/test_coding_standards.py b/ckan/tests/legacy/test_coding_standards.py
--- a/ckan/tests/legacy/test_coding_standards.py
+++ b/ckan/tests/legacy/test_coding_standards.py
@@ -835,7 +835,6 @@ class TestBadExceptions(object):
     # and so should be translated.
 
     NASTY_EXCEPTION_BLACKLIST_FILES = [
-        'ckan/controllers/api.py',
         'ckan/controllers/user.py',
         'ckan/lib/mailer.py',
         'ckan/logic/action/create.py',