From 0b30897cd28bbc62efa2d9811cb3bff8af6627ae Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 2 May 2012 12:42:33 +0100 Subject: [PATCH 001/278] [2347] Implemented related dashboard with filter on type and tidied up some of the related templates --- ckan/config/routing.py | 1 + ckan/controllers/related.py | 50 ++++++++++++++++- ckan/logic/action/get.py | 23 +++++--- ckan/templates/_util.html | 4 +- .../{_snippet => related}/add-related.html | 0 ckan/templates/related/dashboard.html | 56 +++++++++++++++++++ .../{package => related}/related_list.html | 6 +- 7 files changed, 125 insertions(+), 15 deletions(-) rename ckan/templates/{_snippet => related}/add-related.html (100%) create mode 100644 ckan/templates/related/dashboard.html rename ckan/templates/{package => related}/related_list.html (95%) diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 43fa8fd9e1f..1ad271e9c76 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -155,6 +155,7 @@ def make_map(): with SubMapper(map, controller='related') as m: m.connect('related_list', '/dataset/{id}/related', action='list') m.connect('related_read', '/dataset/{id}/related/{related_id}', action='read') + m.connect('related_dashboard', '/apps', action='dashboard') with SubMapper(map, controller='package') as m: m.connect('/dataset', action='search') diff --git a/ckan/controllers/related.py b/ckan/controllers/related.py index 85042aaa8eb..fb6effdd5ee 100644 --- a/ckan/controllers/related.py +++ b/ckan/controllers/related.py @@ -4,13 +4,59 @@ import ckan.logic as logic import ckan.lib.base as base import ckan.lib.helpers as h +import urllib c = base.c class RelatedController(base.BaseController): - def list(self, id): + def dashboard(self): + """ List all related items regardless of dataset """ + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'extras_as_string': True, + 'for_view': True} + data_dict = { + 'type_filter': base.request.params.get('type', '') + } + + params_nopage = [(k, v) for k,v in base.request.params.items() + if k != 'page'] + try: + page = int(base.request.params.get('page', 1)) + except ValueError, e: + abort(400, ('"page" parameter must be an integer')) + + # Update ordering in the context + query = logic.get_action('related_list')(context,data_dict) + + def search_url(params): + url = h.url_for(controller='related', action='dashboard') + params = [(k, v.encode('utf-8') + if isinstance(v, basestring) else str(v)) + for k, v in params] + return url + u'?' + urllib.urlencode(params) + def pager_url(q=None, page=None): + params = list(params_nopage) + params.append(('page', page)) + return search_url(params) + + + c.page = h.Page( + collection=query.all(), + page=page, + url=pager_url, + item_count=query.count(), + items_per_page=8 + ) + + c.filters = dict(params_nopage) + + return base.render( "related/dashboard.html") + + + def list(self, id): + """ List all related items for a specific dataset """ context = {'model': model, 'session': model.Session, 'user': c.user or c.author, 'extras_as_string': True, 'for_view': True} @@ -32,5 +78,5 @@ def list(self, id): c.related_count = len(c.pkg.related) - return base.render( "package/related_list.html") + return base.render( "related/related_list.html") diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index b2afe947171..b0e315e789d 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -138,8 +138,8 @@ def related_show(context, data_dict=None): def related_list(context, data_dict=None): """ - List the related items for a specific package which should be - mentioned in the data_dict + List the related items which may be for a specific package which + should be mentioned in the data_dict context: model - The CKAN model module @@ -150,6 +150,9 @@ def related_list(context, data_dict=None): id - The ID of the dataset to which we want to list related items or dataset - The dataset (package) model + + If neither value is in the data_dict then all related items will + be returned, and the ordering requested will be applied. """ model = context['model'] session = context['session'] @@ -158,14 +161,18 @@ def related_list(context, data_dict=None): if not dataset: dataset = model.Package.get(data_dict.get('id')) - if not dataset: - raise NotFound - check_access('related_show',context, data_dict) - relateds = model.Related.get_for_dataset(dataset, status='active') - related_items = (r.related for r in relateds) - related_list = model_dictize.related_list_dictize( related_items, context) + related_list = [] + if not dataset: + related_list = model.Session.query(model.Related) + tfilter = data_dict.get('type_filter', None) + if tfilter: + related_list = related_list.filter(model.Related.type == tfilter) + else: + relateds = model.Related.get_for_dataset(dataset, status='active') + related_items = (r.related for r in relateds) + related_list = model_dictize.related_list_dictize( related_items, context) return related_list diff --git a/ckan/templates/_util.html b/ckan/templates/_util.html index 5aa7c077c7e..73f976ffea1 100644 --- a/ckan/templates/_util.html +++ b/ckan/templates/_util.html @@ -125,10 +125,10 @@ - +
  • @@ -70,6 +70,6 @@

    Related items - + From db36db63dd994cd1ea84bf294e8b756c0bd41bab Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 2 May 2012 15:07:55 +0100 Subject: [PATCH 002/278] [2347] Added support for sorting by view count, or creation date --- ckan/config/routing.py | 2 +- ckan/controllers/related.py | 25 ++++++++++++++++++++++++- ckan/logic/action/get.py | 19 ++++++++++++++++--- ckan/model/related.py | 1 + ckan/templates/_util.html | 5 +++-- ckan/templates/related/dashboard.html | 15 ++++++++++++++- 6 files changed, 59 insertions(+), 8 deletions(-) diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 1ad271e9c76..dee0f514bef 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -154,7 +154,7 @@ def make_map(): with SubMapper(map, controller='related') as m: m.connect('related_list', '/dataset/{id}/related', action='list') - m.connect('related_read', '/dataset/{id}/related/{related_id}', action='read') + m.connect('related_read', '/apps/{id}', action='read') m.connect('related_dashboard', '/apps', action='dashboard') with SubMapper(map, controller='package') as m: diff --git a/ckan/controllers/related.py b/ckan/controllers/related.py index fb6effdd5ee..3519ad26dd4 100644 --- a/ckan/controllers/related.py +++ b/ckan/controllers/related.py @@ -16,7 +16,8 @@ def dashboard(self): 'user': c.user or c.author, 'extras_as_string': True, 'for_view': True} data_dict = { - 'type_filter': base.request.params.get('type', '') + 'type_filter': base.request.params.get('type', ''), + 'sort': base.request.params.get('sort', '') } params_nopage = [(k, v) for k,v in base.request.params.items() @@ -54,6 +55,28 @@ def pager_url(q=None, page=None): return base.render( "related/dashboard.html") + def read(self, id): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'extras_as_string': True, + 'for_view': True} + data_dict = {'id': id} + + try: + logic.check_access('related_show', context, data_dict) + except logic.NotAuthorized: + abort(401, _('Not authorized to see this page')) + + related = model.Session.query(model.Related).\ + filter(model.Related.id == id).first() + if not related: + abort(404, _('The requested related item was not found')) + + related.view_count += 1 + model.Session.add(related) + model.Session.commit() + + base.redirect(related.url) + def list(self, id): """ List all related items for a specific dataset """ diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index b0e315e789d..2227e4db199 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -166,9 +166,22 @@ def related_list(context, data_dict=None): related_list = [] if not dataset: related_list = model.Session.query(model.Related) - tfilter = data_dict.get('type_filter', None) - if tfilter: - related_list = related_list.filter(model.Related.type == tfilter) + + filter_on_type = data_dict.get('type_filter', None) + if filter_on_type: + related_list = related_list.filter(model.Related.type == filter_on_type) + + sort = data_dict.get('sort', None) + if sort: + sortables = { + 'view_count_asc' : model.Related.view_count.asc, + 'view_count_desc': model.Related.view_count.desc, + 'created_asc' : model.Related.created.asc, + 'created_desc': model.Related.created.desc, + } + s = sortables.get(sort, None) + if s: + related_list = related_list.order_by( s() ) else: relateds = model.Related.get_for_dataset(dataset, status='active') related_items = (r.related for r in relateds) diff --git a/ckan/model/related.py b/ckan/model/related.py index df2d5890317..c7f093ee1a2 100644 --- a/ckan/model/related.py +++ b/ckan/model/related.py @@ -16,6 +16,7 @@ meta.Column('url', meta.UnicodeText), meta.Column('created', meta.DateTime, default=datetime.datetime.now), meta.Column('owner_id', meta.UnicodeText), + meta.Column('view_count', meta.Integer, default=0) ) related_dataset_table = meta.Table('related_dataset', meta.metadata, diff --git a/ckan/templates/_util.html b/ckan/templates/_util.html index 73f976ffea1..1d75af8ae2c 100644 --- a/ckan/templates/_util.html +++ b/ckan/templates/_util.html @@ -129,15 +129,16 @@
  • - + + ${related.view_count}
    ${h.markdown_extract(related.title, extract_length=30)}
    ${h.markdown_extract(related.description, extract_length=1000)}
    No description for this item -

    View this related item

    +

    View this related item

  • diff --git a/ckan/templates/related/dashboard.html b/ckan/templates/related/dashboard.html index fc519b7baec..029b7416b32 100644 --- a/ckan/templates/related/dashboard.html +++ b/ckan/templates/related/dashboard.html @@ -25,7 +25,7 @@

    Showing items ${c.page.first_item} - ${c.p

    ${c.page.item_count} related items found

    -
    + @@ -36,6 +36,19 @@

    ${c.page.item_count} related + + + + +

    From 5a97413dd61e905624dcb1fae95d4cbe180ea5c5 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Thu, 3 May 2012 09:08:12 +0100 Subject: [PATCH 003/278] [2347] viewcount migration and related on user page --- ckan/controllers/user.py | 1 + ckan/lib/dictization/model_dictize.py | 7 +++++++ ckan/migration/versions/058_add_related_viewcount.py | 11 +++++++++++ ckan/templates/user/read.html | 8 ++++++-- 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 ckan/migration/versions/058_add_related_viewcount.py diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py index d17df6fc410..d836a459c06 100644 --- a/ckan/controllers/user.py +++ b/ckan/controllers/user.py @@ -91,6 +91,7 @@ def read(self, id=None): except NotAuthorized: abort(401, _('Not authorized to see this page')) + context['with_related'] = True try: user_dict = get_action('user_show')(context,data_dict) except NotFound: diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index 74aa8b78877..d95950c9a5f 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -355,6 +355,13 @@ def user_dictize(user, context): result_dict['number_of_edits'] = user.number_of_edits() result_dict['number_administered_packages'] = user.number_administered_packages() + model = context['model'] + session = model.Session + + if context.get('with_related'): + result_dict['related_items'] = session.query(model.Related).\ + filter(model.Related.owner_id==user.id).all() + return result_dict def task_status_dictize(task_status, context): diff --git a/ckan/migration/versions/058_add_related_viewcount.py b/ckan/migration/versions/058_add_related_viewcount.py new file mode 100644 index 00000000000..f85493ee66e --- /dev/null +++ b/ckan/migration/versions/058_add_related_viewcount.py @@ -0,0 +1,11 @@ +from sqlalchemy import * +from migrate import * + +def upgrade(migrate_engine): + migrate_engine.execute(''' + ALTER TABLE "related" + ADD COLUMN view_count int NOT NULL DEFAULT 0; + + UPDATE related SET view_count=0 WHERE view_count IS NULL; + ''' + ) diff --git a/ckan/templates/user/read.html b/ckan/templates/user/read.html index 681f0dbf701..91b14c1abee 100644 --- a/ckan/templates/user/read.html +++ b/ckan/templates/user/read.html @@ -1,7 +1,7 @@ - + ${c.user_dict['display_name']} - User ${h.linked_gravatar(c.user_dict['email_hash'],48)} @@ -14,7 +14,7 @@ - +
    @@ -58,6 +58,10 @@ ${c.user_dict['number_of_edits']} Edits

  • +
  • + ${len(c.user_dict['related_items'])} + Related items +
  • From 6002a53a6be3c9a1d9a5940a05377d7f8233d5bc Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Thu, 3 May 2012 11:39:13 +0100 Subject: [PATCH 004/278] [2347] Restrict updates/create of related featured flag to admins for now --- ckan/logic/action/create.py | 6 ++++++ ckan/logic/action/update.py | 5 +++++ ckan/logic/schema.py | 1 + ckan/templates/related/dashboard.html | 2 +- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index aee73a32aa4..5da99b6db83 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -1,6 +1,7 @@ import logging from pylons.i18n import _ +import ckan.authz as authz import ckan.lib.plugins as lib_plugins import ckan.logic as logic import ckan.rating as ratings @@ -126,6 +127,11 @@ def related_create(context, data_dict): model.Session.rollback() raise ValidationError(errors, error_summary(errors)) + # Only sys admins can update a related item to make it 1 + if not authz.Authorizer().is_sysadmin(unicode(user)): + data['featured'] = 0 + + related = model_save.related_dict_save(data, context) if not context.get('defer_commit'): model.repo.commit_and_remove() diff --git a/ckan/logic/action/update.py b/ckan/logic/action/update.py index cd5aa2fe8da..3b839d0a07d 100644 --- a/ckan/logic/action/update.py +++ b/ckan/logic/action/update.py @@ -4,6 +4,7 @@ from pylons.i18n import _ from vdm.sqlalchemy.base import SQLAlchemySession +import ckan.authz as authz import ckan.plugins as plugins import ckan.logic as logic import ckan.logic.schema @@ -120,6 +121,10 @@ def related_update(context, data_dict): model.Session.rollback() raise ValidationError(errors, error_summary(errors)) + # Only sys admins can update a related item to make it 1 + if not authz.Authorizer().is_sysadmin(unicode(user)): + data['featured'] = 0 + related = model_save.related_dict_save(data, context) if not context.get('defer_commit'): model.repo.commit() diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index 09ffe2217ae..4c174e7af27 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -245,6 +245,7 @@ def default_related_schema(): 'url': [ignore_missing, unicode], 'owner_id': [not_empty, unicode], 'created': [ignore], + 'featured': [ignore_missing, unicode], } return schema diff --git a/ckan/templates/related/dashboard.html b/ckan/templates/related/dashboard.html index aa1f0fb2fe6..39b4664be3c 100644 --- a/ckan/templates/related/dashboard.html +++ b/ckan/templates/related/dashboard.html @@ -14,7 +14,7 @@ no-sidebar - Apps + Application Applications From 7cc252d7d5d3c3b7a892f756e8443cc599e7de3c Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Thu, 3 May 2012 14:10:26 +0100 Subject: [PATCH 005/278] [2347] Added activity for creating of related items --- ckan/logic/action/create.py | 23 ++++++++++++++++++- ckan/logic/action/get.py | 6 +++++ ckan/logic/validators.py | 15 ++++++++++++ .../activity_streams/new_related_item.html | 13 +++++++++++ ckan/templates/related/add-related.html | 2 +- ckan/templates/related/dashboard.html | 2 +- 6 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 ckan/templates/activity_streams/new_related_item.html diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index 5da99b6db83..84f99211bd2 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -116,6 +116,7 @@ def resource_create(context, data_dict): def related_create(context, data_dict): model = context['model'] + session = context['session'] user = context['user'] userobj = model.User.get(user) @@ -141,10 +142,30 @@ def related_create(context, data_dict): dataset.related.append( related ) model.repo.commit_and_remove() + session.flush() + + related_dict = model_dictize.related_dictize(related, context) + activity_dict = { + 'user_id': userobj.id, + 'object_id': related.id, + 'activity_type': 'new related item', + } + activity_dict['data'] = { + 'related': related_dict + } + activity_create_context = { + 'model': model, + 'user': user, + 'defer_commit':True, + 'session': session + } + activity_create(activity_create_context, activity_dict, ignore_auth=True) + session.commit() + context["related"] = related context["id"] = related.id log.debug('Created object %s' % str(related.title)) - return model_dictize.related_dictize(related, context) + return related_dict def package_relationship_create(context, data_dict): diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 4d26f89ccab..0cc18d77891 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1245,6 +1245,11 @@ def render_new_package_activity(context, activity): return render('activity_streams/new_package.html', extra_vars = {'activity': activity}) +def render_new_related_activity(context, activity): + return render('activity_streams/new_related_item.html', + extra_vars = {'activity': activity, + 'type': activity['data']['related']['type']}) + def render_deleted_package_activity(context, activity): return render('activity_streams/deleted_package.html', extra_vars = {'activity': activity}) @@ -1347,6 +1352,7 @@ def render_deleted_group_activity(context, activity): 'new group' : render_new_group_activity, 'changed group' : render_changed_group_activity, 'deleted group' : render_deleted_group_activity, + 'new related item': render_new_related_activity } def _activity_list_to_html(context, activity_stream): diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index 58c69ee3aef..3ca2e100529 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -114,6 +114,20 @@ def group_id_exists(group_id, context): raise Invalid('%s: %s' % (_('Not found'), _('Group'))) return group_id + +def related_id_exists(related_id, context): + """Raises Invalid if the given related_id does not exist in the model + given in the context, otherwise returns the given related_id. + + """ + model = context['model'] + session = context['session'] + + result = session.query(model.Related).get(related_id) + if not result: + raise Invalid('%s: %s' % (_('Not found'), _('Related'))) + return related_id + def group_id_or_name_exists(reference, context): """ Raises Invalid if a group identified by the name or id cannot be found. @@ -146,6 +160,7 @@ def activity_type_exists(activity_type): 'new group' : group_id_exists, 'changed group' : group_id_exists, 'deleted group' : group_id_exists, + 'new related item': related_id_exists } def object_id_validator(key, activity_dict, errors, context): diff --git a/ckan/templates/activity_streams/new_related_item.html b/ckan/templates/activity_streams/new_related_item.html new file mode 100644 index 00000000000..be0690782f7 --- /dev/null +++ b/ckan/templates/activity_streams/new_related_item.html @@ -0,0 +1,13 @@ + +${ h.activity_div( + template=_("{actor} created the %s {object}"), + activity=activity, + actor=h.linked_user(activity.user_id), + object=h.link_to(activity.data.related['title'], h.url_for(controller='related', action='read', id=activity.data.related['id'])) + ) % type} + diff --git a/ckan/templates/related/add-related.html b/ckan/templates/related/add-related.html index e5b2a64a1a3..fdda87d0ee3 100644 --- a/ckan/templates/related/add-related.html +++ b/ckan/templates/related/add-related.html @@ -21,7 +21,7 @@

    Add related item

    diff --git a/ckan/templates/related/dashboard.html b/ckan/templates/related/dashboard.html index 39b4664be3c..7149cd6b58d 100644 --- a/ckan/templates/related/dashboard.html +++ b/ckan/templates/related/dashboard.html @@ -31,7 +31,7 @@

    ${c.page.item_count} related From 3eac5416da57d42a4b5ee9ae48b30efffbfc9f6b Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Thu, 3 May 2012 15:30:07 +0100 Subject: [PATCH 006/278] [2347] Added activity for deleting related items --- ckan/logic/action/delete.py | 25 +++++++++++++++++++ ckan/logic/action/get.py | 7 +++++- ckan/logic/validators.py | 3 ++- .../deleted_related_item.html | 14 +++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 ckan/templates/activity_streams/deleted_related_item.html diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index c806a5113cd..1084f4707ec 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -3,11 +3,13 @@ import ckan.logic import ckan.logic.action import ckan.plugins as plugins +import ckan.lib.dictization.model_dictize as model_dictize # define some shortcuts ValidationError = ckan.logic.ValidationError NotFound = ckan.logic.NotFound check_access = ckan.logic.check_access +get_action = ckan.logic.get_action def package_delete(context, data_dict): @@ -65,8 +67,12 @@ def package_relationship_delete(context, data_dict): model.repo.commit() def related_delete(context, data_dict): + import ckan.logic.action as action + model = context['model'] + session = context['session'] user = context['user'] + userobj = model.User.get(user) id = data_dict['id'] entity = model.Related.get(id) @@ -76,6 +82,25 @@ def related_delete(context, data_dict): check_access('related_delete',context, data_dict) + related_dict = model_dictize.related_dictize(entity, context) + activity_dict = { + 'user_id': userobj.id, + 'object_id': entity.id, + 'activity_type': 'deleted related item', + } + activity_dict['data'] = { + 'related': related_dict + } + activity_create_context = { + 'model': model, + 'user': user, + 'defer_commit':True, + 'session': session + } + + get_action('activity_create')(activity_create_context, activity_dict, ignore_auth=True) + session.commit() + entity.delete() model.repo.commit() diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 0cc18d77891..a45b90c63f3 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1262,6 +1262,10 @@ def render_changed_resource_activity(context, activity, detail): return render('activity_streams/changed_resource.html', extra_vars = {'activity': activity, 'detail': detail}) +def render_deleted_related_activity(context, activity): + return render('activity_streams/deleted_related_item.html', + extra_vars = {'activity': activity}) + def render_deleted_resource_activity(context, activity, detail): return render('activity_streams/deleted_resource.html', extra_vars = {'activity': activity, 'detail': detail}) @@ -1352,7 +1356,8 @@ def render_deleted_group_activity(context, activity): 'new group' : render_new_group_activity, 'changed group' : render_changed_group_activity, 'deleted group' : render_deleted_group_activity, - 'new related item': render_new_related_activity + 'new related item': render_new_related_activity, + 'deleted related item': render_deleted_related_activity } def _activity_list_to_html(context, activity_stream): diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index 3ca2e100529..3f58a78df37 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -160,7 +160,8 @@ def activity_type_exists(activity_type): 'new group' : group_id_exists, 'changed group' : group_id_exists, 'deleted group' : group_id_exists, - 'new related item': related_id_exists + 'new related item': related_id_exists, + 'deleted related item': related_id_exists } def object_id_validator(key, activity_dict, errors, context): diff --git a/ckan/templates/activity_streams/deleted_related_item.html b/ckan/templates/activity_streams/deleted_related_item.html new file mode 100644 index 00000000000..bf615e1ad2c --- /dev/null +++ b/ckan/templates/activity_streams/deleted_related_item.html @@ -0,0 +1,14 @@ + +${h.activity_div( + template=_("{actor} deleted the related item {object}"), + activity=activity, + actor=h.linked_user(activity.user_id), + object=activity.data.related.title, + )} + From da04077f943dfd83354b74097c5aa09d9ef7fcbc Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Fri, 4 May 2012 09:56:17 +0100 Subject: [PATCH 007/278] [2347] Added config option to hide related and documented it --- ckan/templates/package/layout.html | 3 +-- doc/configuration.rst | 11 +++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ckan/templates/package/layout.html b/ckan/templates/package/layout.html index 3646d0f5a5d..ae63355a17f 100644 --- a/ckan/templates/package/layout.html +++ b/ckan/templates/package/layout.html @@ -34,8 +34,7 @@ -
  • ${h.subnav_link(h.icon('package') + _('Related') + ' (%s)' % c.related_count, controller='related', action='list', id=c.pkg.name)}
  • - +
  • ${h.subnav_link(h.icon('package') + _('Related') + ' (%s)' % c.related_count, controller='related', action='list', id=c.pkg.name)}
  • |
  • diff --git a/doc/configuration.rst b/doc/configuration.rst index 7bcc74701e3..728cbecda24 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -122,6 +122,17 @@ This sets a space-separated list of extra field key values which will not be sho .. index:: single: rdf_packages +package_show_related +^^^^^^^^^^^^^^^^^^^^ + +package_show_related:: + + package_show_related = 0 + +Default value: 1 + +When set to 0 this setting will hide the related item tab on the package read page. If the value is not set, or is set to 1, then the related item tab will shown. + rdf_packages ^^^^^^^^^^^^ From eeb1cc954e0cc7604edbdcc6fb3a95d3b91c5f73 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Fri, 4 May 2012 10:51:14 +0100 Subject: [PATCH 008/278] [2347] Add a url validator to the related schema --- ckan/logic/schema.py | 7 ++++--- ckan/logic/validators.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index 4c174e7af27..8bcbdfd998b 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -37,7 +37,8 @@ user_id_exists, object_id_validator, activity_type_exists, - tag_not_in_vocabulary) + tag_not_in_vocabulary, + url_validator) from formencode.validators import OneOf import ckan.model @@ -241,8 +242,8 @@ def default_related_schema(): 'title': [not_empty, unicode], 'description': [ignore_missing, unicode], 'type': [not_empty, unicode], - 'image_url': [ignore_missing, unicode], - 'url': [ignore_missing, unicode], + 'image_url': [ignore_missing, unicode, url_validator], + 'url': [ignore_missing, unicode, url_validator], 'owner_id': [not_empty, unicode], 'created': [ignore], 'featured': [ignore_missing, unicode], diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index 3f58a78df37..9451d4d06db 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -492,3 +492,23 @@ def tag_not_in_vocabulary(key, tag_dict, errors, context): (tag_name, vocabulary_id)) else: return + +def url_validator(key, data, errors, context): + """ Checks that the provided value (if it is present) is a valid URL """ + import urlparse + import string + + model = context['model'] + session = context['session'] + + url = data.get(key, None) + if not url: + return + + pieces = urlparse.urlparse(url) + if all([pieces.scheme, pieces.netloc]) and \ + set(pieces.netloc) <= set(string.letters + string.digits + '-.') and \ + pieces.scheme in ['http', 'https']: + return + + errors[key].append(_('Please provide a valid URL')) From d2ea8735a3d01a8135c7635b83f204c92712b878 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Fri, 4 May 2012 11:05:53 +0100 Subject: [PATCH 009/278] [2347] Following advice from tobes to use h.bool to allow use or more than 1 or 0 in ini file --- ckan/templates/package/layout.html | 2 +- doc/configuration.rst | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ckan/templates/package/layout.html b/ckan/templates/package/layout.html index ae63355a17f..5ff630a88ef 100644 --- a/ckan/templates/package/layout.html +++ b/ckan/templates/package/layout.html @@ -34,7 +34,7 @@ -
  • ${h.subnav_link(h.icon('package') + _('Related') + ' (%s)' % c.related_count, controller='related', action='list', id=c.pkg.name)}
  • +
  • ${h.subnav_link(h.icon('package') + _('Related') + ' (%s)' % c.related_count, controller='related', action='list', id=c.pkg.name)}
  • |
  • diff --git a/doc/configuration.rst b/doc/configuration.rst index 728cbecda24..523323c0ce2 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -127,11 +127,11 @@ package_show_related package_show_related:: - package_show_related = 0 + package_show_related = false -Default value: 1 +Default value: true -When set to 0 this setting will hide the related item tab on the package read page. If the value is not set, or is set to 1, then the related item tab will shown. +When set to false, or no, this setting will hide the related item tab on the package read page. If the value is not set, or is set to true or yes, then the related item tab will shown. rdf_packages ^^^^^^^^^^^^ From 45336e342fc1a0dd6a469d67c0438296a2dc6513 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Fri, 4 May 2012 11:14:37 +0100 Subject: [PATCH 010/278] [2347] Fixing up naming --- ckan/templates/package/layout.html | 2 +- doc/configuration.rst | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ckan/templates/package/layout.html b/ckan/templates/package/layout.html index 5ff630a88ef..f196b37e9e7 100644 --- a/ckan/templates/package/layout.html +++ b/ckan/templates/package/layout.html @@ -34,7 +34,7 @@ -
  • ${h.subnav_link(h.icon('package') + _('Related') + ' (%s)' % c.related_count, controller='related', action='list', id=c.pkg.name)}
  • +
  • ${h.subnav_link(h.icon('package') + _('Related') + ' (%s)' % c.related_count, controller='related', action='list', id=c.pkg.name)}
  • |
  • diff --git a/doc/configuration.rst b/doc/configuration.rst index 523323c0ce2..ac2f45e20e1 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -122,12 +122,12 @@ This sets a space-separated list of extra field key values which will not be sho .. index:: single: rdf_packages -package_show_related -^^^^^^^^^^^^^^^^^^^^ +ckan.dataset.show_related +^^^^^^^^^^^^^^^^^^^^^^^^^ -package_show_related:: +ckan.dataset.show_related:: - package_show_related = false + ckan.dataset.show_related = false Default value: true From 8c73fbcda5de09db39efb4469c4e22b0812db87d Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 9 May 2012 10:46:34 +0100 Subject: [PATCH 011/278] [2347] Use auth check in template for add button and make sure related_create for publisher auth is accurate --- ckan/logic/action/create.py | 3 ++- ckan/logic/auth/publisher/create.py | 16 +++++++++++++--- ckan/templates/related/related_list.html | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index 0bc3110016a..2b44b5e2884 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -121,6 +121,8 @@ def related_create(context, data_dict): user = context['user'] userobj = model.User.get(user) + check_access('related_create', context, data_dict) + data_dict["owner_id"] = userobj.id data, errors = validate(data_dict, ckan.logic.schema.default_related_schema(), @@ -133,7 +135,6 @@ def related_create(context, data_dict): if not authz.Authorizer().is_sysadmin(unicode(user)): data['featured'] = 0 - related = model_save.related_dict_save(data, context) if not context.get('defer_commit'): model.repo.commit_and_remove() diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index d1bece7ebaf..a5112a599fe 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -25,10 +25,20 @@ def related_create(context, data_dict=None): user = context['user'] userobj = model.User.get( user ) - if userobj: - return {'success': True} + if not userobj: + return {'success': False, 'msg': _('You must be logged in to add a related item')} + + if 'dataset_id' in data_dict: + # If this is to be associated with a dataset then we need to make sure that + # the user doing so is a member of that group + dataset = model.Package.get(data_dict['dataset_id']) + if dataset and not _groups_intersect( userobj.get_groups(), + dataset.get_groups() ): + return {'success': False, + 'msg': _('You do not have permission to create an item')} + + return {'success': True } - return {'success': False, 'msg': _('You must be logged in to add a related item')} def resource_create(context, data_dict): diff --git a/ckan/templates/related/related_list.html b/ckan/templates/related/related_list.html index 4d18838fc28..f97872fb440 100644 --- a/ckan/templates/related/related_list.html +++ b/ckan/templates/related/related_list.html @@ -23,10 +23,10 @@
    ${add_related(c.pkg)} -

    Related items Add related item

    +

    Related items Add related item

    diff --git a/ckan/templates/package/layout.html b/ckan/templates/package/layout.html index 24b340bcc6f..a5425a4031c 100644 --- a/ckan/templates/package/layout.html +++ b/ckan/templates/package/layout.html @@ -34,7 +34,7 @@ -
  • ${h.subnav_link(h.icon('package') + _('Related') + ' (%s)' % c.related_count, controller='related', action='list', id=c.pkg.name)}
  • +
  • ${h.subnav_link(h.icon('package') + _('Apps & Ideas') + ' (%s)' % c.related_count, controller='related', action='list', id=c.pkg.name)}
  • ${h.subnav_link(h.icon('page_stack') + _('History'), controller='package', action='history', id=c.pkg.name)}
  • ${h.subnav_link( diff --git a/ckan/templates/related/add-related.html b/ckan/templates/related/add-related.html index f7396720d8b..1e5f23e49d5 100644 --- a/ckan/templates/related/add-related.html +++ b/ckan/templates/related/add-related.html @@ -9,7 +9,7 @@ py:def="add_related(dataset)">
  • - ${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 073/278] 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 074/278] 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 075/278] 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 076/278] 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 077/278] 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 078/278] 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 079/278] 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 080/278] 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 081/278] 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 082/278] 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 083/278] 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 084/278] 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 085/278] 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 086/278] 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 087/278] 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 088/278] 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 b094c4eda6e61cacd3da3e638573803457f014d4 Mon Sep 17 00:00:00 2001 From: Toby Date: Tue, 3 Jul 2012 11:18:48 +0100 Subject: [PATCH 089/278] [#2362] toolkit docs --- doc/index.rst | 1 + doc/toolkit.rst | 148 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 doc/toolkit.rst diff --git a/doc/index.rst b/doc/index.rst index e8973a28272..ccd7a8d6dae 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -36,6 +36,7 @@ Customizing and Extending commenting extensions writing-extensions + toolkit forms tag-vocabularies form-integration diff --git a/doc/toolkit.rst b/doc/toolkit.rst new file mode 100644 index 00000000000..29ba543ab00 --- /dev/null +++ b/doc/toolkit.rst @@ -0,0 +1,148 @@ + +Plugins Toolkit +=============== + +To allow a safe way for extensions to interact with ckan a toolkit is +provided. We aim to keep this toolkit stable across ckan versions so +that extensions will work across diferent versions of ckan. + +.. Note:: + + It is advised that when writing extensions that all interaction with + ckan is done via the toolkit so that they do not break when new + versions of ckan are released. + +Over time we will be expanding the functionality available via +this toolkit. + +Example extension that registers a new helper function available to +templates via h.example_helper() :: + + import ckan.plugins as p + + + class ExampleExtension(p.SingletonPlugin): + + p.implements(p.IConfigurer) + p.implements(p.ITemplateHelpers) + + def update_config(self, config): + # add template directory that contains our snippet + p.toolkit.add_template_directory(config, 'templates') + + @classmethod + def example_helper(cls, data=None): + # render our custom snippet + return p.toolkit.render_snippet('custom_snippet.html', data) + + + def get_helpers(self): + # register our helper function + return {'example_helper': self.example_helper} + +The following functions, classes and exceptions are provided by the toolkit. + +*class* **CkanCommand** + Base class for building paster functions. + + +*exception* **CkanVersionException** + Exception raised if required ckan version is not available. + + +*exception* **NotAuthorized** + Exception raised when an action is not permitted by a user. + + +*exception* **ObjectNotFound** + Exception raised when an object cannot be found. + + +*exception* **ValidationError** + Exception raised when supplied data is invalid. + it contains details of the error that occurred. + + +**_** (*value*) + Mark a string for translation. Returns the localized unicode + string of value. + + Mark a string to be localized as follows:: + + _('This should be in lots of languages') + + + + +**add_public_directory** (*config, relative_path*) + Function to aid adding extra public paths to the config. + The path is relative to the file calling this function. + + +**add_template_directory** (*config, relative_path*) + Function to aid adding extra template paths to the config. + The path is relative to the file calling this function. + + +**asbool** (*obj*) + part of paste.deploy.converters: convert strings like yes, no, true, false, 0, 1 to boolean + + +**asint** (*obj*) + part of paste.deploy.converters: convert stings to integers + + +**aslist** (*obj, sep=None, strip=True*) + part of paste.deploy.converters: convert string objects to a list + + +**check_access** (*action, context, data_dict=None*) + check that the named action with the included context and + optional data dict is allowed raises NotAuthorized if the action is + not permitted or True. + + +**check_ckan_version** (*min_version=None, max_version=None*) + Check that the ckan version is correct for the plugin. + + +**get_action** (*action*) + Get the requested action function. + + +*class* **literal** + Represents an HTML literal. + + This subclass of unicode has a ``.__html__()`` method that is + detected by the ``escape()`` function. + + Also, if you add another string to this string, the other string + will be quoted and you will get back another literal object. Also + ``literal(...) % obj`` will quote any value(s) from ``obj``. If + you do something like ``literal(...) + literal(...)``, neither + string will be changed because ``escape(literal(...))`` doesn't + change the original literal. + + Changed in WebHelpers 1.2: the implementation is now now a subclass of + ``markupsafe.Markup``. This brings some new methods: ``.escape`` (class + method), ``.unescape``, and ``.striptags``. + + + + +**render** (*template_name, data=None*) + Main template render function. + + +**render_snippet** (*template_name, data=None*) + helper for the render_snippet function + similar to the render function. + + +**render_text** (*template_name, data=None*) + Render genshi text template. + + +**requires_ckan_version** (*min_version, max_version=None*) + Check that the ckan version is correct for the plugin. + From 3e00ffea420bd11af64b0d2d417e3c6100b71390 Mon Sep 17 00:00:00 2001 From: amercader Date: Mon, 2 Jul 2012 13:05:58 +0100 Subject: [PATCH 090/278] 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 091/278] 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 092/278] 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 093/278] 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 094/278] 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 095/278] 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 9b72e88919fcdd25c0156a430f00968c9ec76ec3 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 6 Jul 2012 14:02:30 +0200 Subject: [PATCH 096/278] [#2627] 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 d93cb58049bf79a1983f3b96759361c0627684a1 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Fri, 6 Jul 2012 14:32:00 +0200 Subject: [PATCH 097/278] 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', ''))} -