From 845c0e5769edb6a1e66a86d97e3ae67f10841363 Mon Sep 17 00:00:00 2001 From: amercader Date: Thu, 31 May 2012 11:13:14 +0100 Subject: [PATCH 001/155] [#2389] Move recline js and css to a snippet This will make easier to modify the libraries and load them conditionally. --- ckan/templates/package/resource_read.html | 36 ++---------------- .../snippets/recline-extra-footer.html | 13 +++++++ .../snippets/recline-extra-header.html | 38 +++++++++++++++++++ 3 files changed, 54 insertions(+), 33 deletions(-) create mode 100644 ckan/templates/snippets/recline-extra-footer.html create mode 100644 ckan/templates/snippets/recline-extra-header.html diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html index bebe246a0ae..cf03b67f502 100644 --- a/ckan/templates/package/resource_read.html +++ b/ckan/templates/package/resource_read.html @@ -15,36 +15,9 @@ - - - - - - - - - + + From d1c82313143b77249223d6da56823c51d1255137 Mon Sep 17 00:00:00 2001 From: "Sven R. Kunze" Date: Mon, 18 Jun 2012 23:34:15 +0200 Subject: [PATCH 002/155] Added route 'user/dashboard' and action 'dashboard' to user controller. --- ckan/config/routing.py | 1 + ckan/controllers/user.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 3890032817f..b529d90eed7 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -252,6 +252,7 @@ def make_map(): m.connect('/user/edit', action='edit') # Note: openid users have slashes in their ids, so need the wildcard # in the route. + m.connect('/user/dashboard', action='dashboard') m.connect('/user/followers/{id:.*}', action='followers') m.connect('/user/edit/{id:.*}', action='edit') m.connect('/user/reset/{id:.*}', action='perform_reset') diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py index c1b81dc3070..ebcfeaa9421 100644 --- a/ckan/controllers/user.py +++ b/ckan/controllers/user.py @@ -444,3 +444,16 @@ def followers(self, id=None): c.followers = get_action('user_follower_list')(context, {'id':c.user_dict['id']}) return render('user/followers.html') + + + def dashboard(self, id=None): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'for_view': True} + data_dict = {'id':id, 'user_obj':c.userobj} + self._setup_template_variables(context, data_dict) + c.followers = get_action('user_follower_list')(context, + {'id':c.user_dict['id']}) + return render('user/followers.html') + + + From b26daa5cc74f9914de317012edd58d1d17283147 Mon Sep 17 00:00:00 2001 From: "Sven R. Kunze" Date: Tue, 19 Jun 2012 15:04:08 +0200 Subject: [PATCH 003/155] New template 'dashboard.html', modified layout --- ckan/controllers/user.py | 4 +--- ckan/templates/user/dashboard.html | 16 ++++++++++++++++ ckan/templates/user/layout.html | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 ckan/templates/user/dashboard.html diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py index ebcfeaa9421..ed967b781e7 100644 --- a/ckan/controllers/user.py +++ b/ckan/controllers/user.py @@ -451,9 +451,7 @@ def dashboard(self, id=None): 'user': c.user or c.author, 'for_view': True} data_dict = {'id':id, 'user_obj':c.userobj} self._setup_template_variables(context, data_dict) - c.followers = get_action('user_follower_list')(context, - {'id':c.user_dict['id']}) - return render('user/followers.html') + return render('user/dashboard.html') diff --git a/ckan/templates/user/dashboard.html b/ckan/templates/user/dashboard.html new file mode 100644 index 00000000000..139443aefca --- /dev/null +++ b/ckan/templates/user/dashboard.html @@ -0,0 +1,16 @@ + + + ${c.user} - Dashboard - User + + ${c.user_dict.display_name}'s Dashboard + + +
+ Hello World. +
+ + + diff --git a/ckan/templates/user/layout.html b/ckan/templates/user/layout.html index a61778d9d39..f0fbd5dcafd 100644 --- a/ckan/templates/user/layout.html +++ b/ckan/templates/user/layout.html @@ -8,6 +8,7 @@ From eba1856526076ddaa5e0ac6667ba0e0a6f8f2b5d Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Wed, 27 Jun 2012 12:23:26 +0200 Subject: [PATCH 073/155] [#2592] Replace pyenv with ~/pyenv in docs --- doc/install-from-source.rst | 21 ++++++++++----------- doc/test.rst | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/doc/install-from-source.rst b/doc/install-from-source.rst index 88ac3d2b1dc..4e14d50a324 100644 --- a/doc/install-from-source.rst +++ b/doc/install-from-source.rst @@ -46,12 +46,11 @@ OpenJDK 6 JDK `The Java Development Kit Date: Wed, 27 Jun 2012 13:23:12 +0100 Subject: [PATCH 074/155] [#2592] stop tip being quoted --- doc/install-from-source.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install-from-source.rst b/doc/install-from-source.rst index 4e14d50a324..64bdc79a3a5 100644 --- a/doc/install-from-source.rst +++ b/doc/install-from-source.rst @@ -80,7 +80,7 @@ the CKAN install. Next you'll need to create a database user if one doesn't already exist. - .. tip :: +.. tip :: If you choose a database name, user or password which are different from the example values suggested below then you'll need to change the From de5c8549995dd178ea910757750876c677a2ff04 Mon Sep 17 00:00:00 2001 From: Toby Date: Wed, 27 Jun 2012 15:18:19 +0100 Subject: [PATCH 075/155] [#2592] add changelog comment --- CHANGELOG.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 21a87c73948..3ac54bb6a6e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,12 @@ CKAN CHANGELOG ++++++++++++++ +v1.8 +==== + +* requirements have been updated see doc/install-from-source.rst +users will need to do a new pip install (#2592) + v1.7.1 2012-06-20 ================= From 152cf537ca09ebad8e8d9fcb6bdf37f8b36feae1 Mon Sep 17 00:00:00 2001 From: John Glover Date: Wed, 27 Jun 2012 15:52:18 +0100 Subject: [PATCH 076/155] [1732] Fix tag facet list when using vocab tags. Removes vocab tags from tag list when indexing. Adds vocab tags to their own dynamic field called vocab_. --- ckan/config/solr/CHANGELOG.txt | 5 ++ ckan/config/solr/schema-1.4.xml | 2 + ckan/lib/search/index.py | 26 +++++++-- ckan/tests/logic/test_tag_vocab.py | 85 +++++++++++++++++++++++++++--- 4 files changed, 105 insertions(+), 13 deletions(-) diff --git a/ckan/config/solr/CHANGELOG.txt b/ckan/config/solr/CHANGELOG.txt index 1e4e67f6ff6..735d6a8dc6c 100644 --- a/ckan/config/solr/CHANGELOG.txt +++ b/ckan/config/solr/CHANGELOG.txt @@ -1,6 +1,11 @@ CKAN SOLR schemas changelog =========================== +v1.4 - (ckan>=1.8) +------------------ +* Add vocab_* dynamic field so it is possible to facet by vocabulary tags +* Add copyField for text with source vocab_* + v1.4 - (ckan>=1.7) -------------------- * Add Ascii folding filter to text fields. diff --git a/ckan/config/solr/schema-1.4.xml b/ckan/config/solr/schema-1.4.xml index 0409e71b14b..d98b9c56f5c 100644 --- a/ckan/config/solr/schema-1.4.xml +++ b/ckan/config/solr/schema-1.4.xml @@ -153,6 +153,7 @@ + @@ -165,6 +166,7 @@ + diff --git a/ckan/lib/search/index.py b/ckan/lib/search/index.py index e9129009947..09ec2eec913 100644 --- a/ckan/lib/search/index.py +++ b/ckan/lib/search/index.py @@ -1,7 +1,6 @@ import socket import string import logging -import itertools import collections import json @@ -14,6 +13,7 @@ import ckan.model as model from ckan.plugins import (PluginImplementations, IPackageController) +import ckan.logic as logic log = logging.getLogger(__name__) @@ -122,10 +122,27 @@ def index_package(self, pkg_dict): pkg_dict[key] = value pkg_dict.pop('extras', None) - #Add tags and groups + # add tags, removing vocab tags from 'tags' list and adding them as + # vocab_ so that they can be used in facets + non_vocab_tag_names = [] tags = pkg_dict.pop('tags', []) - pkg_dict['tags'] = [tag['name'] for tag in tags] - + context = {'model': model} + + for tag in tags: + if tag.get('vocabulary_id'): + data = {'id': tag['vocabulary_id']} + vocab = logic.get_action('vocabulary_show')(context, data) + key = u'vocab_%s' % vocab['name'] + if key in pkg_dict: + pkg_dict[key].append(tag['name']) + else: + pkg_dict[key] = [tag['name']] + else: + non_vocab_tag_names.append(tag['name']) + + pkg_dict['tags'] = non_vocab_tag_names + + # add groups groups = pkg_dict.pop('groups', []) # Capacity is different to the default only if using organizations @@ -197,7 +214,6 @@ def index_package(self, pkg_dict): import hashlib pkg_dict['index_id'] = hashlib.md5('%s%s' % (pkg_dict['id'],config.get('ckan.site_id'))).hexdigest() - for item in PluginImplementations(IPackageController): pkg_dict = item.before_index(pkg_dict) diff --git a/ckan/tests/logic/test_tag_vocab.py b/ckan/tests/logic/test_tag_vocab.py index 4500ea69a7f..9f806e3ce54 100644 --- a/ckan/tests/logic/test_tag_vocab.py +++ b/ckan/tests/logic/test_tag_vocab.py @@ -1,13 +1,17 @@ -from ckan import model -from ckan.logic.converters import convert_to_tags, convert_from_tags, free_tags_only -from ckan.lib.navl.dictization_functions import unflatten +import ckan.model as model +import ckan.logic as logic +import ckan.logic.converters as converters +import ckan.lib.navl.dictization_functions as df +import ckan.tests as tests +import ckan.lib.create_test_data as ctd TEST_VOCAB_NAME = 'test-vocab' + class TestConverters(object): @classmethod def setup_class(cls): - cls.vocab = model.Vocabulary(TEST_VOCAB_NAME) + cls.vocab = model.Vocabulary(TEST_VOCAB_NAME) model.Session.add(cls.vocab) model.Session.commit() vocab_tag_1 = model.Tag('tag1', cls.vocab.id) @@ -27,11 +31,11 @@ def convert(tag_string, vocab): data = {key: tag_string} errors = [] context = {'model': model, 'session': model.Session} - convert_to_tags(vocab)(key, data, errors, context) + converters.convert_to_tags(vocab)(key, data, errors, context) del data[key] return data - data = unflatten(convert(['tag1', 'tag2'], 'test-vocab')) + data = df.unflatten(convert(['tag1', 'tag2'], 'test-vocab')) for tag in data['tags']: assert tag['name'] in ['tag1', 'tag2'], tag['name'] assert tag['vocabulary_id'] == self.vocab.id, tag['vocabulary_id'] @@ -44,7 +48,7 @@ def test_convert_from_tags(self): } errors = [] context = {'model': model, 'session': model.Session} - convert_from_tags(self.vocab.name)(key, data, errors, context) + converters.convert_from_tags(self.vocab.name)(key, data, errors, context) assert 'tag1' in data['tags'] assert 'tag2' in data['tags'] @@ -58,8 +62,73 @@ def test_free_tags_only(self): } errors = [] context = {'model': model, 'session': model.Session} - free_tags_only(key, data, errors, context) + converters.free_tags_only(key, data, errors, context) assert len(data) == 2 assert ('tags', 1, 'vocabulary_id') in data.keys() assert ('tags', 1, '__extras') in data.keys() + +class TestVocabFacets(object): + @classmethod + def setup_class(cls): + if not tests.is_search_supported(): + raise tests.SkipTest("Search not supported") + tests.setup_test_search_index() + + ctd.CreateTestData.create() + + model.repo.new_revision() + cls.vocab = model.Vocabulary(TEST_VOCAB_NAME) + model.Session.add(cls.vocab) + model.Session.commit() + + vocab_tag_1 = model.Tag('tag1', cls.vocab.id) + vocab_tag_2 = model.Tag('tag2', cls.vocab.id) + model.Session.add(vocab_tag_1) + model.Session.add(vocab_tag_2) + + pkg = model.Package.get('warandpeace') + pkg_tag1 = model.PackageTag(pkg, vocab_tag_1) + pkg_tag2 = model.PackageTag(pkg, vocab_tag_2) + model.Session.add(pkg_tag1) + model.Session.add(pkg_tag2) + + model.Session.commit() + model.Session.remove() + + @classmethod + def teardown_class(cls): + model.repo.rebuild_db() + + def test_vocab_facets(self): + vocab_facet = 'vocab_%s' % TEST_VOCAB_NAME + + context = {'model': model, 'session': model.Session} + data = { + 'q': 'warandpeace', + 'facet': 'true', + 'facet.field': ['groups', 'tags', vocab_facet], + 'facet.limit': '50', + 'facet.mincount': 1, + } + + result = logic.get_action('package_search')(context, data) + facets = result['search_facets'] + facet_tags = [t['name'] for t in facets['tags']['items']] + assert len(facet_tags) + + # make sure vocab tags are not in 'tags' facet + assert not 'tag1' in facet_tags + assert not 'tag2' in facet_tags + + # make sure vocab tags are in vocab_ facet + vocab_facet_tags = [t['name'] for t in facets[vocab_facet]['items']] + assert 'tag1' in vocab_facet_tags + assert 'tag2' in vocab_facet_tags + + def test_vocab_facets_searchable(self): + context = {'model': model, 'session': model.Session} + data = {'q': 'tag1', 'facet': 'false'} + result = logic.get_action('package_search')(context, data) + assert result['count'] == 1 + assert result['results'][0]['name'] == 'warandpeace' From a03c9605ab7d7ecbae90b1d40f0f052a73b0f410 Mon Sep 17 00:00:00 2001 From: "Sven R. Kunze" Date: Wed, 27 Jun 2012 17:00:01 +0200 Subject: [PATCH 077/155] Dashboard now available through API; API tests modified --- ckan/controllers/api.py | 1 + ckan/lib/helpers.py | 2 +- ckan/logic/action/get.py | 2 +- ckan/tests/functional/api/test_activity.py | 86 ++++++++++++++++++++++ 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/ckan/controllers/api.py b/ckan/controllers/api.py index 723d4bbf6cc..66754fdb1f2 100644 --- a/ckan/controllers/api.py +++ b/ckan/controllers/api.py @@ -251,6 +251,7 @@ def list(self, ver=None, register=None, subregister=None, id=None): ('dataset', 'activity'): 'package_activity_list', ('group', 'activity'): 'group_activity_list', ('user', 'activity'): 'user_activity_list', + ('user', 'dashboard_activity'): 'dashboard_activity_list', ('activity', 'details'): 'activity_detail_list', } diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 6a3d520b4fe..114efb0a78e 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -883,7 +883,7 @@ def dashboard_activity_stream(user_id): ''' import ckan.logic as logic context = {'model' : model, 'session':model.Session, 'user':c.user} - return logic.get_action('dashboard_activity_list_html')(context, {'user_id': user_id}) + return logic.get_action('dashboard_activity_list_html')(context, {'id': user_id}) # these are the functions that will end up in `h` template helpers diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 79ff3edabd6..a8a6fa9c506 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -2054,7 +2054,7 @@ def dashboard_activity_list(context, data_dict): ''' model = context['model'] - user_id = _get_or_bust(data_dict, 'user_id') + user_id = _get_or_bust(data_dict, 'id') activity_query = model.Session.query(model.Activity) user_query = activity_query; diff --git a/ckan/tests/functional/api/test_activity.py b/ckan/tests/functional/api/test_activity.py index 64ff6a2f743..4814b064d0e 100644 --- a/ckan/tests/functional/api/test_activity.py +++ b/ckan/tests/functional/api/test_activity.py @@ -96,14 +96,51 @@ def setup_class(self): ckan.tests.CreateTestData.create() self.sysadmin_user = model.User.get('testsysadmin') self.normal_user = model.User.get('annafan') + self.follower = model.User.get('tester') self.warandpeace = model.Package.get('warandpeace') self.annakarenina = model.Package.get('annakarenina') self.app = paste.fixture.TestApp(pylonsapp) + # Make follower follow everything else. + params = {'id': 'testsysadmin'} + extra_environ = {'Authorization': str(self.follower.apikey)} + response = self.app.post('/api/action/follow_user', + params=json.dumps(params), extra_environ=extra_environ).json + assert response['success'] is True + params = {'id': 'annafan'} + extra_environ = {'Authorization': str(self.follower.apikey)} + response = self.app.post('/api/action/follow_user', + params=json.dumps(params), extra_environ=extra_environ).json + assert response['success'] is True + params = {'id': 'warandpeace'} + extra_environ = {'Authorization': str(self.follower.apikey)} + response = self.app.post('/api/action/follow_dataset', + params=json.dumps(params), extra_environ=extra_environ).json + assert response['success'] is True + params = {'id': 'annakarenina'} + extra_environ = {'Authorization': str(self.follower.apikey)} + response = self.app.post('/api/action/follow_dataset', + params=json.dumps(params), extra_environ=extra_environ).json + assert response['success'] is True + + self.followees = \ + [ + self.sysadmin_user.id, + self.normal_user.id, + self.follower.id, + self.warandpeace.id, + self.annakarenina.id + ] + + @classmethod def teardown_class(self): model.repo.rebuild_db() + def dashboard_activity_stream(self, user_id): + response = self.app.get("/api/2/rest/user/%s/dashboard_activity" % user_id) + return json.loads(response.body) + def user_activity_stream(self, user_id): response = self.app.get("/api/2/rest/user/%s/activity" % user_id) return json.loads(response.body) @@ -145,9 +182,25 @@ def record_details(self, user_id, package_id=None, group_id=None): details['recently changed datasets stream'] = \ self.recently_changed_datasets_stream() + details['follower dashboard activity stream'] = \ + self.dashboard_activity_stream(self.follower.id) + details['time'] = datetime.datetime.now() return details + def check_dashboard( + self, + before, after, wanted_difference, + potential_followees): + difference = find_new_activities( + before['follower dashboard activity stream'], + after['follower dashboard activity stream']) + if any(potential_followee in self.followees for potential_followee in potential_followees): + assert difference == wanted_difference + else: + assert len(difference) == 0 + + def _create_package(self, user, name=None): if user: user_name = user.name @@ -189,6 +242,8 @@ def _create_package(self, user, name=None): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [user_id]) + # Check that the new activity has the right attributes. assert activity['object_id'] == package_created['id'], \ str(activity['object_id']) @@ -297,6 +352,8 @@ def _add_resource(self, package, user): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [user_id, package.id]) + # Check that the new activity has the right attributes. assert activity['object_id'] == updated_package['id'], \ str(activity['object_id']) @@ -386,6 +443,8 @@ def _delete_extra(self, package_dict, user): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [user_id, package_dict['id']]) + # Check that the new activity has the right attributes. assert activity['object_id'] == updated_package['id'], \ str(activity['object_id']) @@ -480,6 +539,8 @@ def _update_extra(self, package_dict, user): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [user_id, package_dict['id']]) + # Check that the new activity has the right attributes. assert activity['object_id'] == updated_package['id'], \ str(activity['object_id']) @@ -570,6 +631,8 @@ def _add_extra(self, package_dict, user, key=None): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [user_id, package_dict['id']]) + # Check that the new activity has the right attributes. assert activity['object_id'] == updated_package['id'], \ str(activity['object_id']) @@ -628,6 +691,8 @@ def _create_activity(self, user, package, params): after['package activity stream'])) assert pkg_new_activities == user_new_activities + self.check_dashboard(before, after, user_new_activities, [user.id, package.id]) + # Check that the new activity has the right attributes. assert activity['object_id'] == params['object_id'], ( str(activity['object_id'])) @@ -671,6 +736,8 @@ def _delete_group(self, group, user): after['group activity stream']) == new_activities, ("The same " "activity should also appear in the group's activity stream.") + self.check_dashboard(before, after, new_activities, [user.id]) + # Check that the new activity has the right attributes. assert activity['object_id'] == group.id, str(activity['object_id']) assert activity['user_id'] == user.id, str(activity['user_id']) @@ -712,6 +779,9 @@ def _update_group(self, group, user): after['group activity stream']) == new_activities, ("The same " "activity should also appear in the group's activity stream.") + self.check_dashboard(before, after, new_activities, [user.id]) + + # Check that the new activity has the right attributes. assert activity['object_id'] == group.id, str(activity['object_id']) assert activity['user_id'] == user.id, str(activity['user_id']) @@ -758,6 +828,8 @@ def _update_user(self, user): "the user's activity stream, but found %i" % len(new_activities)) activity = new_activities[0] + self.check_dashboard(before, after, new_activities, [user.id]) + # Check that the new activity has the right attributes. assert activity['object_id'] == user.id, str(activity['object_id']) assert activity['user_id'] == user.id, str(activity['user_id']) @@ -822,6 +894,8 @@ def _delete_resources(self, package): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [package.id]) + # Check that the new activity has the right attributes. assert activity['object_id'] == package.id, ( str(activity['object_id'])) @@ -905,6 +979,8 @@ def _update_package(self, package, user): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [user_id, package.id]) + # Check that the new activity has the right attributes. assert activity['object_id'] == package.id, ( str(activity['object_id'])) @@ -981,6 +1057,8 @@ def _update_resource(self, package, resource, user): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [user_id, package.id]) + # Check that the new activity has the right attributes. assert activity['object_id'] == package.id, ( str(activity['object_id'])) @@ -1049,6 +1127,8 @@ def _delete_package(self, package): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [self.sysadmin_user.id, package.id]) + # Check that the new activity has the right attributes. assert activity['object_id'] == package.id, ( str(activity['object_id'])) @@ -1160,6 +1240,8 @@ def test_01_remove_tag(self): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [user.id, pkg_dict['id']]) + # Check that the new activity has the right attributes. assert activity['object_id'] == pkg_dict['id'], ( str(activity['object_id'])) @@ -1416,6 +1498,8 @@ def test_create_group(self): assert after['group activity stream'] == new_activities, ("The same " "activity should also appear in the group's activity stream.") + self.check_dashboard(before, after, new_activities, [user.id]) + # Check that the new activity has the right attributes. assert activity['object_id'] == group_created['id'], \ str(activity['object_id']) @@ -1496,6 +1580,8 @@ def test_add_tag(self): after['recently changed datasets stream']) \ == user_new_activities + self.check_dashboard(before, after, user_new_activities, [user.id, pkg_dict['id']]) + # Check that the new activity has the right attributes. assert activity['object_id'] == pkg_dict['id'], ( str(activity['object_id'])) From 1c3361322b6be982615ac793db2b2fc1a456585a Mon Sep 17 00:00:00 2001 From: "Sven R. Kunze" Date: Wed, 27 Jun 2012 17:45:27 +0200 Subject: [PATCH 078/155] brief test for dashboard activtities --- ckan/tests/functional/test_activity.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ckan/tests/functional/test_activity.py b/ckan/tests/functional/test_activity.py index b7f59724ac2..fccdfb42f93 100644 --- a/ckan/tests/functional/test_activity.py +++ b/ckan/tests/functional/test_activity.py @@ -25,8 +25,9 @@ def setup(cls): def teardown(cls): ckan.model.repo.rebuild_db() - def test_activity(self): - """Test activity streams HTML rendering.""" + + def test_user_activity(self): + """Test user activity streams HTML rendering.""" # Register a new user. user_dict = {'name': 'billybeane', @@ -208,3 +209,10 @@ def test_activity(self): result = self.app.get(offset, status=200) assert result.body.count('
') \ == 15 + + # The latest 15 should also appear on the dashboard + offset = url_for(controller='user', action='dashboard') + params = {'id': user['id']} + extra_environ = {'Authorization': str(self.sysadmin_user.apikey)} + response = self.app.post(offset, params=params, extra_environ=extra_environ, status=200) + assert result.body.count('
') == 15 From bad89a82900b154a86606b13d219d81e6893fbe0 Mon Sep 17 00:00:00 2001 From: "Sven R. Kunze" Date: Wed, 27 Jun 2012 18:19:01 +0200 Subject: [PATCH 079/155] user encouragement added --- ckan/templates/user/dashboard.html | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ckan/templates/user/dashboard.html b/ckan/templates/user/dashboard.html index 2d20308f987..289d6516769 100644 --- a/ckan/templates/user/dashboard.html +++ b/ckan/templates/user/dashboard.html @@ -21,5 +21,16 @@

What's going on?

+
+

Nothing new on CKAN?

+

So, why don't you ...

+ +
+ From bd817df3cda3b0fc2e7a20d0b4808c022215cf50 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 27 Jun 2012 17:53:55 +0100 Subject: [PATCH 080/155] [#2389] Try to make the data explorer fail with more dignity Adds some checks for DataStore and DataProxy backends. Recline does not make easy to catch exceptions and errors while initializing, but hopefully this will cover the most common cases. Note: includes a small patch on recline.js, which will be ported upstream. --- ckan/public/scripts/application.js | 39 +++++++++++++++++-- ckan/public/scripts/vendor/recline/recline.js | 5 +++ ckan/templates/js_strings.html | 6 ++- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index 11202ad634d..0eb88162420 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -1553,6 +1553,14 @@ CKAN.DataPreview = function ($, my) { my.loadPreviewDialog = function(resourceData) { my.$dialog.html('

Loading ...

'); + function showError(msg){ + msg = msg || CKAN.Strings.errorLoadingPreview; + return $('#ckanext-datapreview') + .append('
') + .addClass('alert alert-error fade in') + .html(msg); + } + function initializeDataExplorer(dataset) { var views = [ { @@ -1586,6 +1594,7 @@ CKAN.DataPreview = function ($, my) { } }); + // ----------------------------- // Setup the Embed modal dialog. // ----------------------------- @@ -1665,14 +1674,38 @@ CKAN.DataPreview = function ($, my) { if (resourceData.webstore_url) { resourceData.elasticsearch_url = '/api/data/' + resourceData.id; var dataset = new recline.Model.Dataset(resourceData, 'elasticsearch'); - initializeDataExplorer(dataset); + var errorMsg = CKAN.Strings.errorLoadingPreview + ': ' + CKAN.Strings.errorDataStore; + dataset.fetch() + .done(function(dataset){ + initializeDataExplorer(dataset); + }) + .fail(function(error){ + if (error.message) errorMsg += ' (' + error.message + ')'; + showError(errorMsg); + }); + } else if (resourceData.formatNormalized in {'csv': '', 'xls': ''}) { // set format as this is used by Recline in setting format for DataProxy resourceData.format = resourceData.formatNormalized; var dataset = new recline.Model.Dataset(resourceData, 'dataproxy'); - initializeDataExplorer(dataset); - $('.recline-query-editor .text-query').hide(); + var errorMsg = CKAN.Strings.errorLoadingPreview + ': ' +CKAN.Strings.errorDataProxy; + dataset.fetch() + .done(function(dataset){ + + dataset.bind('query:fail', function(error) { + $('#ckanext-datapreview .data-view-container').hide(); + $('#ckanext-datapreview .header').hide(); + $('.preview-header .btn').hide(); + }); + + initializeDataExplorer(dataset); + $('.recline-query-editor .text-query').hide(); + }) + .fail(function(error){ + if (error.message) errorMsg += ' (' + error.message + ')'; + showError(errorMsg); + }); } else if (resourceData.formatNormalized in { 'rdf+xml': '', diff --git a/ckan/public/scripts/vendor/recline/recline.js b/ckan/public/scripts/vendor/recline/recline.js index 271e9c54fb9..39ca7c9c737 100644 --- a/ckan/public/scripts/vendor/recline/recline.js +++ b/ckan/public/scripts/vendor/recline/recline.js @@ -3177,6 +3177,11 @@ this.recline.Backend = this.recline.Backend || {}; var dfd = $.Deferred(); this._wrapInTimeout(jqxhr).done(function(schema) { // only one top level key in ES = the type so we can ignore it + // CKAN + if (!schema){ + dfd.reject({'message':'Elastic Search did not return a mapping'}); + return; + } var key = _.keys(schema)[0]; var fieldData = _.map(schema[key].properties, function(dict, fieldName) { dict.id = fieldName; diff --git a/ckan/templates/js_strings.html b/ckan/templates/js_strings.html index 61db8cf8e29..77314fb993f 100644 --- a/ckan/templates/js_strings.html +++ b/ckan/templates/js_strings.html @@ -66,7 +66,11 @@ deleteResource = _('Delete Resource'), youCanUseMarkdown = _('You can use %aMarkdown formatting%b here.'), datesAreInISO = _('Dates are in %aISO Format%b — eg. %c2012-12-25%d or %c2010-05-31T14:30%d.'), - dataFileUploaded = _('Data File (Uploaded)') + dataFileUploaded = _('Data File (Uploaded)'), + errorLoadingPreview = _('Could not load preview'), + errorDataProxy = _('DataProxy returned an error'), + errorDataStore = _('DataStore returned an error') + ), indent=4)} From 7f59a58e15dfc1d2498534ef0d474cc7ea08d2b9 Mon Sep 17 00:00:00 2001 From: amercader Date: Wed, 27 Jun 2012 18:13:57 +0100 Subject: [PATCH 081/155] Update datastore docs --- doc/datastore.rst | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/doc/datastore.rst b/doc/datastore.rst index 15fcd8fe35c..9f3de6a919c 100644 --- a/doc/datastore.rst +++ b/doc/datastore.rst @@ -66,19 +66,41 @@ Please see the ElasticSearch_ documentation. 2. Configure Nginx ------------------ -You must add to your Nginx CKAN site entry the following:: - - location /elastic/ { - internal; - # location of elastic search - proxy_pass http://0.0.0.0:9200/; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +As previously mentioned, Nginx will be used on top of CKAN to forward +requests to Elastic Search. CKAN will still be served by Apache or the +development server (Paster), but all requests will be forwarded to it +by Ngnix. + +This is an example of an Nginx configuration file. Note the two locations +defined, `/` will point to the server running CKAN (Apache or Paster), and +`/elastic/` to the Elastic Search instance:: + + server { + listen 80 default; + server_name localhost; + + access_log /var/log/nginx/localhost.access.log; + + location / { + # location of apache or ckan under paster + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + } + location /elastic/ { + internal; + # location of elastic search + proxy_pass http://:127.0.0.1:9200/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } } .. note:: update the proxy_pass field value to point to your ElasticSearch instance (if it is not localhost and default port). +Remember that after setting up Nginx, you need to access CKAN via its port +(80), not the Apache or Paster (5000) one, otherwise the DataStore won't work. + 3. Enable datastore features in CKAN ------------------------------------ From 57edefe385abf276c6874e6f7ce30b7621eca122 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 28 Jun 2012 11:14:15 +0200 Subject: [PATCH 082/155] Remove print statements from test_user.py --- ckan/tests/functional/test_user.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ckan/tests/functional/test_user.py b/ckan/tests/functional/test_user.py index 1ee29522b22..8206d317676 100644 --- a/ckan/tests/functional/test_user.py +++ b/ckan/tests/functional/test_user.py @@ -3,7 +3,6 @@ from pylons import config import hashlib -from pprint import pprint from ckan.tests import search_related, CreateTestData from ckan.tests.html_check import HtmlCheckMethods from ckan.tests.pylons_controller import PylonsTestCase @@ -192,7 +191,6 @@ def test_login(self): res.header('Location').startswith('/user/dashboard') res = res.follow() assert_equal(res.status, 200) - print res.body assert 'testlogin is now logged in' in res.body assert 'checkpoint:my-dashboard' in res.body @@ -208,9 +206,7 @@ def test_login(self): assert 'testlogin!userid_type:unicode' in cookie, cookie # navigate to another page and check username still displayed - print res.body res = res.click('Search') - print res assert 'testlogin' in res.body, res.body def test_login_remembered(self): @@ -948,7 +944,6 @@ def test_reset_user_password_link(self): create_reset_key(model.User.by_name(u'bob')) reset_password_link = get_reset_link(model.User.by_name(u'bob')) offset = reset_password_link.replace('http://test.ckan.net', '') - print offset res = self.app.get(offset) # Reset password form From 3cc2f81745260c3569991758452adf32b5fb0289 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 28 Jun 2012 11:25:30 +0200 Subject: [PATCH 083/155] [#2347] Don't add foreign keys to activity model Don't want to have to do a db migrate for this branch --- ckan/model/activity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckan/model/activity.py b/ckan/model/activity.py index f4b6a7ef715..ff72337f04d 100644 --- a/ckan/model/activity.py +++ b/ckan/model/activity.py @@ -14,8 +14,8 @@ 'activity', meta.metadata, Column('id', types.UnicodeText, primary_key=True, default=_types.make_uuid), Column('timestamp', types.DateTime), - Column('user_id', types.UnicodeText, ForeignKey('user.id')), - Column('object_id', types.UnicodeText, ForeignKey('package.id')), + Column('user_id', types.UnicodeText), + Column('object_id', types.UnicodeText), Column('revision_id', types.UnicodeText), Column('activity_type', types.UnicodeText), Column('data', _types.JsonDictType), From 59b3297bae4461b47037071db26ec80e18eef81a Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 28 Jun 2012 11:42:38 +0200 Subject: [PATCH 084/155] [#2347] Remove an unnecessary route, fix link --- ckan/config/routing.py | 1 - ckan/templates/user/layout.html | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 5b372a65804..b529d90eed7 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -253,7 +253,6 @@ def make_map(): # Note: openid users have slashes in their ids, so need the wildcard # in the route. m.connect('/user/dashboard', action='dashboard') - m.connect('/user/read', action='read') m.connect('/user/followers/{id:.*}', action='followers') m.connect('/user/edit/{id:.*}', action='edit') m.connect('/user/reset/{id:.*}', action='perform_reset') diff --git a/ckan/templates/user/layout.html b/ckan/templates/user/layout.html index b4ec82523ed..e733d4df336 100644 --- a/ckan/templates/user/layout.html +++ b/ckan/templates/user/layout.html @@ -9,7 +9,7 @@ - ${facet_div('tags', 'Tags')} - ${facet_div('res_format', 'Resource Formats')} + ${facet_div('tags', _('Tags'))} + ${facet_div('res_format', _('Resource Formats'))} From 4594319109fd4e2cd8f152216ae9214e22a8c2ca Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 2 Jul 2012 15:38:18 +0200 Subject: [PATCH 123/155] Add strings from core extensions to pot file --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a8f8297e035..74187be3b0c 100644 --- a/setup.py +++ b/setup.py @@ -45,8 +45,13 @@ }), ('public/**', 'ignore', None), ], - 'ckanext/stats/templates': [ + 'ckanext': [ + ('**.py', 'python', None), ('**.html', 'genshi', None), + ('multilingual/solr/*.txt', 'ignore', None), + ('**.txt', 'genshi', { + 'template_class': 'genshi.template:TextTemplate' + }), ]}, entry_points=""" [nose.plugins.0.10] From fb782e2043d58c7015eb17ee6fe236316dc79ea9 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 2 Jul 2012 15:38:52 +0200 Subject: [PATCH 124/155] Fix a string that had a lot of whitespace in it --- ckanext/organizations/controllers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckanext/organizations/controllers.py b/ckanext/organizations/controllers.py index fbe6e25e464..371b2141d98 100644 --- a/ckanext/organizations/controllers.py +++ b/ckanext/organizations/controllers.py @@ -95,8 +95,8 @@ def apply(self, id=None, data=None, errors=None, error_summary=None): def _add_users( self, group, parameters ): if not group: - h.flash_error(_("There was a problem with your submission, \ - please correct it and try again")) + h.flash_error(_("There was a problem with your submission, " + "please correct it and try again")) errors = {"reason": ["No reason was supplied"]} return self.apply(group.id, errors=errors, error_summary=action.error_summary(errors)) From 58d054970a1ff108678ae8ee3124c7f27eca224a Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Mon, 25 Jun 2012 17:42:38 +0100 Subject: [PATCH 125/155] Dictize results list of resource_search action. Looks like this action wasn't even working! --- ckan/logic/action/get.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index f15525bd768..cb215d0546b 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1240,7 +1240,8 @@ def resource_search(context, data_dict): else: results.append(result) - return {'count': count, 'results': results} + return {'count': count, + 'results': model_dictize.resource_list_dictize(results, context)} def _tag_search(context, data_dict): model = context['model'] From f386122015e1b8408b1f94046d8422eb2d17ac7a Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Mon, 25 Jun 2012 17:44:20 +0100 Subject: [PATCH 126/155] Docstring for resource_search action. --- ckan/logic/action/get.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index cb215d0546b..1737b76bb17 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1178,18 +1178,24 @@ def package_search(context, data_dict): def resource_search(context, data_dict): ''' + Searches for resources satisfying a given search criteria. + + It returns a dictionary with 2 fields: ``count`` and ``results``. The + ``count`` field contains the total number of Resources found without the + limit or query parameters having an effect. The ``results`` field is a + list of dictized Resource objects. :param fields: :type fields: - :param order_by: - :type order_by: - :param offset: - :type offset: - :param limit: - :type limit: - - :returns: - :rtype: + :param order_by: A field on the Resource model that orders the results. + :type order_by: string + :param offset: Apply an offset to the query. + :type offset: int + :param limit: Apply a limit to the query. + :type limit: int + + :returns: A dictionary with a ``count`` field, and a ``results`` field. + :rtype: dict ''' model = context['model'] From ebd9bc613ea4cea5617998cf2c669ff3f267f4dd Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Tue, 26 Jun 2012 15:43:55 +0100 Subject: [PATCH 127/155] Add 'q' parameter to resource_search action. This deprecates the existing 'fields' parameter, which is not compatible with GET-ing the resource_search action. --- ckan/logic/action/get.py | 56 +++++++++++++++++++++++++++-- ckan/tests/logic/test_action.py | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 1737b76bb17..37d0ac0a37e 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1185,8 +1185,52 @@ def resource_search(context, data_dict): limit or query parameters having an effect. The ``results`` field is a list of dictized Resource objects. - :param fields: - :type fields: + The 'q' parameter is a required field. It is a string of the form + ``{field}:{term}`` or a list of strings, each of the same form. Within + each string, ``{field}`` is a field or extra field on the Resource domain + object. + + If ``{field}`` is ``"hash"``, then an attempt is made to match the + `{term}` as a *prefix* of the ``Resource.hash`` field. + + If ``{field}`` is an extra field, then an attempt is made to match against + the extra fields stored against the Resource. + + Note: The search is limited to search against extra fields declared in + the config setting ``ckan.extra_resource_fields``. + + Note: Due to a Resource's extra fields being stored as a json blob, the + match is made against the json string representation. As such, false + positives may occur: + + If the search criteria is: :: + + query = "field1:term1" + + Then a json blob with the string representation of: :: + + {"field1": "foo", "field2": "term1"} + + will match the search criteria! This is a known short-coming of this + approach. + + All matches are made ignoring case; and apart from the ``"hash"`` field, + a term matches if it is a substring of the field's value. + + Finally, when specifying more than one search criteria, the criteria are + AND-ed together. + + The ``order`` parameter is used to control the ordering of the results. + Currently only ordering one field is available, and in ascending order + only. + + The ``fields`` parameter is deprecated as it is not compatible with calling + this action with a GET request to the action API. + + :param query: The search criteria. See above for description. + :type query: string or list of strings of the form "{field}:{term1}" + :param fields: Deprecated + :type fields: dict of fields to search terms. :param order_by: A field on the Resource model that orders the results. :type order_by: string :param offset: Apply an offset to the query. @@ -1201,15 +1245,21 @@ def resource_search(context, data_dict): model = context['model'] session = context['session'] + query = _get_or_bust(data_dict, 'query') fields = _get_or_bust(data_dict, 'fields') order_by = data_dict.get('order_by') offset = data_dict.get('offset') limit = data_dict.get('limit') + if isinstance(query, basestring): + query = [query] + + # TODO: escape ':' with '\:' + fields = dict(pair.split(":", 1) for pair in query) + # TODO: should we check for user authentication first? q = model.Session.query(model.Resource) resource_fields = model.Resource.get_columns() - for field, terms in fields.items(): if isinstance(terms, basestring): terms = terms.split() diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index d104c5d3e94..c8dc581c341 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -1023,6 +1023,69 @@ def test_41_missing_action(self): except KeyError: assert True + def test_42_resource_search_with_single_field_query(self): + request_body = { + 'q': ["description:index"], + 'fields': [] + } + postparams = json.dumps(request_body) + response = self.app.post('/api/action/resource_search', + params=postparams) + result = json.loads(response.body)['result']['results'] + + ## Due to the side-effect of previously run tests, there may be extra + ## resources in the results. So just check that each found Resource + ## matches the search criteria + assert result is not [] + for resource in result: + assert "index" in resource['description'].lower() + + def test_42_resource_search_across_multiple_fields(self): + request_body = { + 'q': ["description:index", "format:json"], + 'fields': [] + } + postparams = json.dumps(request_body) + response = self.app.post('/api/action/resource_search', + params=postparams) + result = json.loads(response.body)['result']['results'] + + ## Due to the side-effect of previously run tests, there may be extra + ## resources in the results. So just check that each found Resource + ## matches the search criteria + assert result is not [] + for resource in result: + assert "index" in resource['description'].lower() + assert "json" in resource['format'].lower() + + def test_42_resource_search_test_percentage_is_escaped(self): + pass + + def test_42_resource_search_escaped_colons(self): + pass + + def test_42_resource_search_fields_parameter_still_accepted(self): + '''The fields parameter is deprecated, but check it still works. + + Remove this test when removing the fields parameter. (#????) + ''' + request_body = { + 'fields': [("description", "index")], + 'q': [], + } + + postparams = json.dumps(request_body) + response = self.app.post('/api/action/resource_search', + params=postparams) + result = json.loads(response.body)['result']['results'] + + ## Due to the side-effect of previously run tests, there may be extra + ## resources in the results. So just check that each found Resource + ## matches the search criteria + assert result is not [] + for resource in result: + assert "index" in resource['description'].lower() + class TestActionTermTranslation(WsgiAppCase): @classmethod From cc070212a450b7c716cf79e81b29ba54301709be Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Tue, 26 Jun 2012 16:52:49 +0100 Subject: [PATCH 128/155] Ensure legacy parameter, fields, still works. Keep the old fields `parameter's` behaviour, but encourage use of the new `query` parameter. --- ckan/logic/action/get.py | 38 +++++++++++++++++++++++++-------- ckan/tests/logic/test_action.py | 9 +++----- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 37d0ac0a37e..c0657748ce9 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1245,24 +1245,44 @@ def resource_search(context, data_dict): model = context['model'] session = context['session'] - query = _get_or_bust(data_dict, 'query') - fields = _get_or_bust(data_dict, 'fields') + # Allow either query or fields parameter to be given, but not both. + # Once ``fields`` parameter is dropped, this can be made simpler. + # The result of all this gumpf is to populate the local `fields` variable + # with mappings from field names to list of search terms, or a single + # search-term string. + query = data_dict.get('query') + fields = data_dict.get('fields') + if query is None and fields is None: + raise ValidationError({'query': _('Missing value')}) + elif query is not None and fields is not None: + raise ValidationError( + {'fields': _('Do not specify if using "query" parameter')}) + elif query is not None: + if isinstance(query, basestring): + query = [query] + + # TODO: escape ':' with '\:' + fields = dict(pair.split(":", 1) for pair in query) + else: + # legacy fields paramter would split string terms + # maintain that behaviour + split_terms = {} + for field, terms in fields.items(): + if isinstance(terms, basestring): + terms = terms.split() + split_terms[field] = terms + fields = split_terms + order_by = data_dict.get('order_by') offset = data_dict.get('offset') limit = data_dict.get('limit') - if isinstance(query, basestring): - query = [query] - - # TODO: escape ':' with '\:' - fields = dict(pair.split(":", 1) for pair in query) - # TODO: should we check for user authentication first? q = model.Session.query(model.Resource) resource_fields = model.Resource.get_columns() for field, terms in fields.items(): if isinstance(terms, basestring): - terms = terms.split() + terms = [terms] if field not in resource_fields: raise search.SearchError('Field "%s" not recognised in Resource search.' % field) for term in terms: diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index c8dc581c341..acb4104d964 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -1025,8 +1025,7 @@ def test_41_missing_action(self): def test_42_resource_search_with_single_field_query(self): request_body = { - 'q': ["description:index"], - 'fields': [] + 'query': ["description:index"], } postparams = json.dumps(request_body) response = self.app.post('/api/action/resource_search', @@ -1042,8 +1041,7 @@ def test_42_resource_search_with_single_field_query(self): def test_42_resource_search_across_multiple_fields(self): request_body = { - 'q': ["description:index", "format:json"], - 'fields': [] + 'query': ["description:index", "format:json"], } postparams = json.dumps(request_body) response = self.app.post('/api/action/resource_search', @@ -1070,8 +1068,7 @@ def test_42_resource_search_fields_parameter_still_accepted(self): Remove this test when removing the fields parameter. (#????) ''' request_body = { - 'fields': [("description", "index")], - 'q': [], + 'fields': {"description": "index"}, } postparams = json.dumps(request_body) From f8450f4f2b11308e340c83bd78d114e513717615 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Tue, 26 Jun 2012 17:42:28 +0100 Subject: [PATCH 129/155] Special chars are escaped in search terms. --- ckan/logic/action/get.py | 1 + ckan/tests/logic/test_action.py | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index c0657748ce9..5c03b07b63e 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1286,6 +1286,7 @@ def resource_search(context, data_dict): if field not in resource_fields: raise search.SearchError('Field "%s" not recognised in Resource search.' % field) for term in terms: + term = misc.escape_sql_like_special_characters(term) model_attr = getattr(model.Resource, field) if field == 'hash': q = q.filter(model_attr.ilike(unicode(term) + '%')) diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index acb4104d964..330238aa661 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -1031,11 +1031,12 @@ def test_42_resource_search_with_single_field_query(self): response = self.app.post('/api/action/resource_search', params=postparams) result = json.loads(response.body)['result']['results'] + count = json.loads(response.body)['result']['count'] ## Due to the side-effect of previously run tests, there may be extra ## resources in the results. So just check that each found Resource ## matches the search criteria - assert result is not [] + assert count > 0 for resource in result: assert "index" in resource['description'].lower() @@ -1047,20 +1048,29 @@ def test_42_resource_search_across_multiple_fields(self): response = self.app.post('/api/action/resource_search', params=postparams) result = json.loads(response.body)['result']['results'] + count = json.loads(response.body)['result']['count'] ## Due to the side-effect of previously run tests, there may be extra ## resources in the results. So just check that each found Resource ## matches the search criteria - assert result is not [] + assert count > 0 for resource in result: assert "index" in resource['description'].lower() assert "json" in resource['format'].lower() def test_42_resource_search_test_percentage_is_escaped(self): - pass + request_body = { + 'query': ["description:index%"], + } + postparams = json.dumps(request_body) + response = self.app.post('/api/action/resource_search', + params=postparams) + count = json.loads(response.body)['result']['count'] - def test_42_resource_search_escaped_colons(self): - pass + # There shouldn't be any results. If the '%' character wasn't + # escaped correctly, then the search would match because of the + # unescaped wildcard. + assert count is 0 def test_42_resource_search_fields_parameter_still_accepted(self): '''The fields parameter is deprecated, but check it still works. @@ -1075,11 +1085,12 @@ def test_42_resource_search_fields_parameter_still_accepted(self): response = self.app.post('/api/action/resource_search', params=postparams) result = json.loads(response.body)['result']['results'] + count = json.loads(response.body)['result']['count'] ## Due to the side-effect of previously run tests, there may be extra ## resources in the results. So just check that each found Resource ## matches the search criteria - assert result is not [] + assert count > 0 for resource in result: assert "index" in resource['description'].lower() From aeef82cbf16656308730b3e3b9121a4828b526c2 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Tue, 26 Jun 2012 17:53:41 +0100 Subject: [PATCH 130/155] Useful validation errors for resource_search --- ckan/logic/action/get.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 5c03b07b63e..69ad1cd4949 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1260,9 +1260,11 @@ def resource_search(context, data_dict): elif query is not None: if isinstance(query, basestring): query = [query] - - # TODO: escape ':' with '\:' - fields = dict(pair.split(":", 1) for pair in query) + try: + fields = dict(pair.split(":", 1) for pair in query) + except ValueError: + raise ValidationError( + {'query': _('Must be {field}:{value} pair(s)')}) else: # legacy fields paramter would split string terms # maintain that behaviour @@ -1284,7 +1286,10 @@ def resource_search(context, data_dict): if isinstance(terms, basestring): terms = [terms] if field not in resource_fields: - raise search.SearchError('Field "%s" not recognised in Resource search.' % field) + raise ValidationError( + {'query': + _('Field "{field}" not recognised in resource_search.')\ + .format(field=field)}) for term in terms: term = misc.escape_sql_like_special_characters(term) model_attr = getattr(model.Resource, field) From 966ab39183351701656786d63a193bf79898c748 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Tue, 26 Jun 2012 17:54:23 +0100 Subject: [PATCH 131/155] Remove unused session object. --- ckan/logic/action/get.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 69ad1cd4949..fc7ca2a1090 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1243,7 +1243,6 @@ def resource_search(context, data_dict): ''' model = context['model'] - session = context['session'] # Allow either query or fields parameter to be given, but not both. # Once ``fields`` parameter is dropped, this can be made simpler. From 79505e541263eadce6f7245e6548240499413ca1 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Tue, 26 Jun 2012 18:00:48 +0100 Subject: [PATCH 132/155] General tidy of resource_search action. --- ckan/logic/action/get.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index fc7ca2a1090..82963437153 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1244,18 +1244,21 @@ def resource_search(context, data_dict): ''' model = context['model'] - # Allow either query or fields parameter to be given, but not both. - # Once ``fields`` parameter is dropped, this can be made simpler. + # Allow either the `query` or `fields` parameter to be given, but not both. + # Once `fields` parameter is dropped, this can be made simpler. # The result of all this gumpf is to populate the local `fields` variable # with mappings from field names to list of search terms, or a single # search-term string. query = data_dict.get('query') fields = data_dict.get('fields') + if query is None and fields is None: raise ValidationError({'query': _('Missing value')}) + elif query is not None and fields is not None: raise ValidationError( {'fields': _('Do not specify if using "query" parameter')}) + elif query is not None: if isinstance(query, basestring): query = [query] @@ -1264,9 +1267,10 @@ def resource_search(context, data_dict): except ValueError: raise ValidationError( {'query': _('Must be {field}:{value} pair(s)')}) + else: - # legacy fields paramter would split string terms - # maintain that behaviour + # The legacy fields paramter splits string terms. + # So maintain that behaviour split_terms = {} for field, terms in fields.items(): if isinstance(terms, basestring): @@ -1282,18 +1286,29 @@ def resource_search(context, data_dict): q = model.Session.query(model.Resource) resource_fields = model.Resource.get_columns() for field, terms in fields.items(): + if isinstance(terms, basestring): terms = [terms] + if field not in resource_fields: raise ValidationError( {'query': _('Field "{field}" not recognised in resource_search.')\ .format(field=field)}) + for term in terms: + + # prevent pattern injection term = misc.escape_sql_like_special_characters(term) + model_attr = getattr(model.Resource, field) + + # Treat the has field separately, see docstring. if field == 'hash': q = q.filter(model_attr.ilike(unicode(term) + '%')) + + # Resource extras are stored in a json blob. So searching for + # matching fields is a bit trickier. See the docstring. elif field in model.Resource.get_extra_columns(): model_attr = getattr(model.Resource, 'extras') @@ -1302,6 +1317,8 @@ def resource_search(context, data_dict): model_attr.ilike(u'''%%"%s": "%%%s%%"}''' % (field, term)) ) q = q.filter(like) + + # Just a regular field else: q = q.filter(model_attr.ilike('%' + unicode(term) + '%')) From d4ea63a2b9c2f5dd339879269fb366f847b3ab76 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Tue, 26 Jun 2012 18:05:41 +0100 Subject: [PATCH 133/155] Add reference to ticket number --- ckan/tests/logic/test_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index 330238aa661..c9b672126b6 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -1075,7 +1075,7 @@ def test_42_resource_search_test_percentage_is_escaped(self): def test_42_resource_search_fields_parameter_still_accepted(self): '''The fields parameter is deprecated, but check it still works. - Remove this test when removing the fields parameter. (#????) + Remove this test when removing the fields parameter. (#2603) ''' request_body = { 'fields': {"description": "index"}, From dcca75713e44763645c2190cd531b30f8c99ff31 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Tue, 26 Jun 2012 18:18:15 +0100 Subject: [PATCH 134/155] Add log message warning of deprecated parameter. Obviously this won't indicate to the action api users, only when in library access. --- ckan/logic/action/get.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 82963437153..b8825331da9 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1269,6 +1269,9 @@ def resource_search(context, data_dict): {'query': _('Must be {field}:{value} pair(s)')}) else: + log.warning('Use of the "fields" parameter in resource_search is ' + 'deprecated. Use the "query" parameter instead') + # The legacy fields paramter splits string terms. # So maintain that behaviour split_terms = {} From 575a96470a9278dc660144743999b9c2849e114d Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Tue, 26 Jun 2012 18:50:28 +0100 Subject: [PATCH 135/155] Context determines resource_search's result type. This is provided as it allows the search api to continue to use this action, and still provide the required result-type for the action api. --- ckan/lib/search/query.py | 6 +++++- ckan/logic/action/get.py | 25 ++++++++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/ckan/lib/search/query.py b/ckan/lib/search/query.py index a3b7adf35fe..4d11961d01d 100644 --- a/ckan/lib/search/query.py +++ b/ckan/lib/search/query.py @@ -186,7 +186,11 @@ def run(self, fields={}, options=None, **kwargs): else: options.update(kwargs) - context = {'model':model, 'session': model.Session} + context = { + 'model':model, + 'session': model.Session, + 'search_query': True, + } data_dict = { 'fields': fields, 'offset': options.get('offset'), diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index b8825331da9..d84cc1cd759 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1227,6 +1227,11 @@ def resource_search(context, data_dict): The ``fields`` parameter is deprecated as it is not compatible with calling this action with a GET request to the action API. + The context may contain a flag, `search_query`, which if True will make + this action behave as if being used by the internal search api. ie - the + results will not be dictized, and SearchErrors are thrown for bad search + queries (rather than ValidationErrors). + :param query: The search criteria. See above for description. :type query: string or list of strings of the form "{field}:{term1}" :param fields: Deprecated @@ -1294,10 +1299,16 @@ def resource_search(context, data_dict): terms = [terms] if field not in resource_fields: - raise ValidationError( - {'query': - _('Field "{field}" not recognised in resource_search.')\ - .format(field=field)}) + msg = _('Field "{field}" not recognised in resource_search.')\ + .format(field=field) + + # Running in the context of the internal search api. + if context.get('search_query', False): + raise search.SearchError(msg) + + # Otherwise, assume we're in the context of an external api + # and need to provide meaningful external error messages. + raise ValidationError({'query': msg}) for term in terms: @@ -1341,8 +1352,12 @@ def resource_search(context, data_dict): else: results.append(result) + # If run in the context of a search query, then don't dictize the results. + if not context.get('search_query', False): + results = model_dictize.resource_list_dictize(results, context) + return {'count': count, - 'results': model_dictize.resource_list_dictize(results, context)} + 'results': results} def _tag_search(context, data_dict): model = context['model'] From aad834c25ce6f28cd719a59d3d89f5cc79ab533c Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Tue, 26 Jun 2012 19:13:22 +0100 Subject: [PATCH 136/155] Test resource_search works with a GET request --- ckan/tests/logic/test_action.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index c9b672126b6..3b3566fecf7 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -1094,6 +1094,21 @@ def test_42_resource_search_fields_parameter_still_accepted(self): for resource in result: assert "index" in resource['description'].lower() + def test_42_resource_search_accessible_via_get_request(self): + response = self.app.get('/api/action/resource_search' + '?query=description:index&query=format:json') + + result = json.loads(response.body)['result']['results'] + count = json.loads(response.body)['result']['count'] + + ## Due to the side-effect of previously run tests, there may be extra + ## resources in the results. So just check that each found Resource + ## matches the search criteria + assert count > 0 + for resource in result: + assert "index" in resource['description'].lower() + assert "json" in resource['format'].lower() + class TestActionTermTranslation(WsgiAppCase): @classmethod From f4b6a025618d4ae874f4ffb452365cc6301b4aa6 Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Wed, 27 Jun 2012 10:54:35 +0100 Subject: [PATCH 137/155] Use the new 'query' parameter in resource_search instead of the now deprecated 'fields' paramter. --- ckan/lib/search/query.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ckan/lib/search/query.py b/ckan/lib/search/query.py index 4d11961d01d..1380e531bb1 100644 --- a/ckan/lib/search/query.py +++ b/ckan/lib/search/query.py @@ -191,8 +191,18 @@ def run(self, fields={}, options=None, **kwargs): 'session': model.Session, 'search_query': True, } + + # Transform fields into structure required by the resource_search + # action. + query = [] + for field, terms in fields.items(): + if isinstance(terms, basestring): + terms = terms.split() + for term in terms: + query.append(':'.join([field, term])) + data_dict = { - 'fields': fields, + 'query': query, 'offset': options.get('offset'), 'limit': options.get('limit'), 'order_by': options.get('order_by') From 80781b39423084f33cacd981e8e88e9a69c4d1ce Mon Sep 17 00:00:00 2001 From: Ian Murray Date: Wed, 27 Jun 2012 10:56:36 +0100 Subject: [PATCH 138/155] Clarify translatable string. ``{field}`` is usually used to represent a substitution. --- ckan/logic/action/get.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index d84cc1cd759..829a7906828 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1271,7 +1271,7 @@ def resource_search(context, data_dict): fields = dict(pair.split(":", 1) for pair in query) except ValueError: raise ValidationError( - {'query': _('Must be {field}:{value} pair(s)')}) + {'query': _('Must be : pair(s)')}) else: log.warning('Use of the "fields" parameter in resource_search is ' From 3e00ffea420bd11af64b0d2d417e3c6100b71390 Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 2 Jul 2012 13:05:58 +0100 Subject: [PATCH 139/155] Make synchronous search the default behaviour Unless already loaded or explicitly disabled via the `ckan.search.automatic_index` configuration option, the synchronous search plugin will be loaded during startup time. --- ckan/config/deployment.ini_tmpl | 7 ++++++- ckan/config/environment.py | 8 ++++++++ doc/configuration.rst | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ckan/config/deployment.ini_tmpl b/ckan/config/deployment.ini_tmpl index d3c0199687f..d66d6543248 100644 --- a/ckan/config/deployment.ini_tmpl +++ b/ckan/config/deployment.ini_tmpl @@ -24,7 +24,7 @@ app_instance_uuid = ${app_instance_uuid} # List the names of CKAN extensions to activate. # Note: This line is required to be here for packaging, even if it is empty. -ckan.plugins = stats synchronous_search +ckan.plugins = stats # If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings @@ -112,6 +112,11 @@ ckan.gravatar_default = identicon ## Solr support #solr_url = http://127.0.0.1:8983/solr +## Automatic indexing. Make all changes immediately available via the search +## after editing or creating a dataset. Default is true. If for some reason +## you need the indexing to occur asynchronously, set this option to 0. +# ckan.search.automatic_indexing = 1 + ## An 'id' for the site (using, for example, when creating entries in a common search index) ## If not specified derived from the site_url # ckan.site_id = ckan.net diff --git a/ckan/config/environment.py b/ckan/config/environment.py index bb46ea2db8a..8fca75b7548 100644 --- a/ckan/config/environment.py +++ b/ckan/config/environment.py @@ -19,6 +19,7 @@ import ckan.lib.search as search import ckan.lib.app_globals as app_globals +log = logging.getLogger(__name__) # Suppress benign warning 'Unbuilt egg for setuptools' warnings.simplefilter('ignore', UserWarning) @@ -122,6 +123,13 @@ def find_controller(self, controller): # load all CKAN plugins p.load_all(config) + # Load the synchronous search plugin, unless already loaded or + # explicitly disabled + if not 'synchronous_search' in config.get('ckan.plugins') and \ + asbool(config.get('ckan.search.automatic_indexing',True)): + log.debug('Loading the synchronous search plugin') + p.load('synchronous_search') + for plugin in p.PluginImplementations(p.IConfigurer): # must do update in place as this does not work: # config = plugin.update_config(config) diff --git a/doc/configuration.rst b/doc/configuration.rst index 9b753e480d9..ebb09845bdc 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -496,6 +496,23 @@ Optionally, ``solr_user`` and ``solr_password`` can also be configured to specif Note, if you change this value, you need to rebuild the search index. +.. index:: + single: ckan.search.automatic_indexing + +ckan.search.automatic_indexing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Example:: + + ckan.search.automatic_indexing = 1 + +Make all changes immediately available via the search after editing or +creating a dataset. Default is true. If for some reason you need the indexing +to occur asynchronously, set this option to 0. + +Note, this is equivalent to explicitly load the `synchronous_search` plugin. + + simple_search ^^^^^^^^^^^^^ From 08cba3144db70784f90c381a2139c8c035f2b63e Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 2 Jul 2012 13:44:37 +0100 Subject: [PATCH 140/155] PEP8 environment.py --- ckan/config/environment.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/ckan/config/environment.py b/ckan/config/environment.py index 8fca75b7548..d388937ff33 100644 --- a/ckan/config/environment.py +++ b/ckan/config/environment.py @@ -24,6 +24,7 @@ # Suppress benign warning 'Unbuilt egg for setuptools' warnings.simplefilter('ignore', UserWarning) + class _Helpers(object): ''' Helper object giving access to template helpers stopping missing functions from causing template exceptions. Useful if @@ -94,13 +95,16 @@ def load_environment(global_conf, app_conf): from pylons.wsgiapp import PylonsApp import pkg_resources find_controller_generic = PylonsApp.find_controller + # This is from pylons 1.0 source, will monkey-patch into 0.9.7 def find_controller(self, controller): if controller in self.controller_classes: return self.controller_classes[controller] # Check to see if its a dotted name if '.' in controller or ':' in controller: - mycontroller = pkg_resources.EntryPoint.parse('x=%s' % controller).load(False) + mycontroller = pkg_resources \ + .EntryPoint \ + .parse('x=%s' % controller).load(False) self.controller_classes[controller] = mycontroller return mycontroller return find_controller_generic(self, controller) @@ -126,7 +130,7 @@ def find_controller(self, controller): # Load the synchronous search plugin, unless already loaded or # explicitly disabled if not 'synchronous_search' in config.get('ckan.plugins') and \ - asbool(config.get('ckan.search.automatic_indexing',True)): + asbool(config.get('ckan.search.automatic_indexing', True)): log.debug('Loading the synchronous search plugin') p.load('synchronous_search') @@ -160,11 +164,13 @@ def find_controller(self, controller): config['pylons.app_globals'] = app_globals.Globals() # add helper functions - restrict_helpers = asbool(config.get('ckan.restrict_template_vars', 'true')) + restrict_helpers = asbool( + config.get('ckan.restrict_template_vars', 'true')) helpers = _Helpers(h, restrict_helpers) config['pylons.h'] = helpers - ## redo template setup to use genshi.search_path (so remove std template setup) + # Redo template setup to use genshi.search_path + # (so remove std template setup) template_paths = [paths['templates'][0]] extra_template_paths = config.get('extra_template_paths', '') if extra_template_paths: @@ -173,6 +179,7 @@ def find_controller(self, controller): # Translator (i18n) translator = Translator(pylons.translator) + def template_loaded(template): translator.setup(template) @@ -203,8 +210,6 @@ def template_loaded(template): # # ################################################################# - - ''' This code is based on Genshi code @@ -273,11 +278,14 @@ def genshi_lookup_attr(cls, obj, key): # Setup the SQLAlchemy database engine # Suppress a couple of sqlalchemy warnings - warnings.filterwarnings('ignore', '^Unicode type received non-unicode bind param value', sqlalchemy.exc.SAWarning) - warnings.filterwarnings('ignore', "^Did not recognize type 'BIGINT' of column 'size'", sqlalchemy.exc.SAWarning) - warnings.filterwarnings('ignore', "^Did not recognize type 'tsvector' of column 'search_vector'", sqlalchemy.exc.SAWarning) + msgs = ['^Unicode type received non-unicode bind param value', + "^Did not recognize type 'BIGINT' of column 'size'", + "^Did not recognize type 'tsvector' of column 'search_vector'" + ] + for msg in msgs: + warnings.filterwarnings('ignore', msg, sqlalchemy.exc.SAWarning) - ckan_db = os.environ.get('CKAN_DB') + ckan_db = os.environ.get('CKAN_DB') if ckan_db: config['sqlalchemy.url'] = ckan_db @@ -296,4 +304,3 @@ def genshi_lookup_attr(cls, obj, key): for plugin in p.PluginImplementations(p.IConfigurable): plugin.configure(config) - From 63d82ceaea33277b21be3d336f1cd29d98959704 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 28 Jun 2012 18:53:24 +0200 Subject: [PATCH 141/155] Update several dependencies Replaced PyUtilib==4.0.2848 with pyutilib.component.core==4.5.3, this avoids pulling in all the pyutilib packages and the packages they depend on. Updated webhelpers, solrpy, formalchemy, markupsafe, tempita, routes and paste to the latest versions from pypi --- pip-requirements.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pip-requirements.txt b/pip-requirements.txt index 8a9fc42ce87..98f4a9d7150 100644 --- a/pip-requirements.txt +++ b/pip-requirements.txt @@ -5,23 +5,23 @@ Genshi==0.6 sqlalchemy-migrate==0.7.1 sqlalchemy==0.7.3 -webhelpers==1.2 -PyUtilib==4.0.2848 +webhelpers==1.3 +pyutilib.component.core==4.5.3 -e git+https://github.com/okfn/vdm.git@vdm-0.11#egg=vdm -solrpy==0.9.4 -formalchemy==1.4.1 +solrpy==0.9.5 +formalchemy==1.4.2 pairtree==0.7.1-T ofs==0.4.1 apachemiddleware==0.1.1 -markupsafe==0.9.2 +markupsafe==0.15 babel==0.9.4 psycopg2==2.0.13 webob==1.0.8 Pylons==0.9.7 repoze.who==1.0.19 -tempita==0.4 +tempita==0.5.1 zope.interface==3.5.3 repoze.who.plugins.openid==0.5.3 repoze.who-friendlyform==1.0.8 -routes==1.12 -paste==1.7.2 +routes==1.13 +paste==1.7.5.1 From a8dab115f0e94b7f475d15ffd6897bd03a6f81b4 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 28 Jun 2012 19:23:03 +0200 Subject: [PATCH 142/155] Update babel, psyco and zope dependencies --- pip-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pip-requirements.txt b/pip-requirements.txt index 98f4a9d7150..3156743d773 100644 --- a/pip-requirements.txt +++ b/pip-requirements.txt @@ -14,13 +14,13 @@ pairtree==0.7.1-T ofs==0.4.1 apachemiddleware==0.1.1 markupsafe==0.15 -babel==0.9.4 -psycopg2==2.0.13 +babel==0.9.6 +psycopg2==2.4.5 webob==1.0.8 Pylons==0.9.7 repoze.who==1.0.19 tempita==0.5.1 -zope.interface==3.5.3 +zope.interface==4.0.1 repoze.who.plugins.openid==0.5.3 repoze.who-friendlyform==1.0.8 routes==1.13 From 755431512cf806b3af77a3942b613da369f719f8 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 29 Jun 2012 06:18:03 +0000 Subject: [PATCH 143/155] Upgrade sqlalchemy-migrate to 0.7.2 (latest) --- pip-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip-requirements.txt b/pip-requirements.txt index 3156743d773..5db1fc4c09a 100644 --- a/pip-requirements.txt +++ b/pip-requirements.txt @@ -3,7 +3,7 @@ # like: pip install -r pip-requirements.txt. See the Install from Source # instructions in CKAN's documentation for full installation instructions. Genshi==0.6 -sqlalchemy-migrate==0.7.1 +sqlalchemy-migrate==0.7.2 sqlalchemy==0.7.3 webhelpers==1.3 pyutilib.component.core==4.5.3 From 5629aade36916b967ffae250812e833816e0dddd Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 29 Jun 2012 06:18:42 +0000 Subject: [PATCH 144/155] Upgrade sqlalchemy to 0.7.8 (latest) --- pip-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pip-requirements.txt b/pip-requirements.txt index 5db1fc4c09a..2bb53343f1a 100644 --- a/pip-requirements.txt +++ b/pip-requirements.txt @@ -4,7 +4,7 @@ # instructions in CKAN's documentation for full installation instructions. Genshi==0.6 sqlalchemy-migrate==0.7.2 -sqlalchemy==0.7.3 +sqlalchemy==0.7.8 webhelpers==1.3 pyutilib.component.core==4.5.3 -e git+https://github.com/okfn/vdm.git@vdm-0.11#egg=vdm From cd7974b2be434d88d937f1fbad2042413b463c48 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 6 Jul 2012 14:02:30 +0200 Subject: [PATCH 145/155] Refactor form_to_db_schema_options Make it call self.form_to_db_schema so that the form_to_db_schema() methods of IDatasetForm extensions get called. Also make it call new form_to_db_schema_api_create() and form_to_db_schema_api_update() methods which could potentially be overridden by extensions also. --- ckan/lib/plugins.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py index 258e1449681..4ce1f4236ca 100644 --- a/ckan/lib/plugins.py +++ b/ckan/lib/plugins.py @@ -221,11 +221,21 @@ def form_to_db_schema_options(self, options): if options.get('api'): if options.get('type') == 'create': - return logic.schema.default_create_package_schema() + return self.form_to_db_schema_api_create() else: - return logic.schema.default_update_package_schema() + assert options.get('type') == 'update' + return self.form_to_db_schema_api_update() else: - return logic.schema.package_form_schema() + return self.form_to_db_schema() + + def form_to_db_schema(self, options): + return logic.schema.package_form_schema() + + def form_to_db_schema_api_create(self): + return logic.schema.default_create_package_schema() + + def form_to_db_schema_api_update(self): + return logic.schema.default_update_package_schema() def db_to_form_schema(self): '''This is an interface to manipulate data from the database From 90012807433047593f7acac22522af5bc1302ac6 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 6 Jul 2012 14:32:00 +0200 Subject: [PATCH 146/155] Fix a crasher in package/read.html template Sometimes c.pkg_dict has no member named groups (e.g. when there is an active IDatasetForm plugin with a db_to_form_schema() method). --- ckan/templates/package/read.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/templates/package/read.html b/ckan/templates/package/read.html index 29a03720bbd..7c76ef39192 100644 --- a/ckan/templates/package/read.html +++ b/ckan/templates/package/read.html @@ -45,7 +45,7 @@

Tags

${tag_list(c.pkg_dict.get('tags', ''))} -