diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 2915b1b997c..f3f8508c6f4 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -4,32 +4,38 @@ import datetime from urllib import urlencode -from ckan.lib.base import BaseController, c, model, request, render, g -from ckan.lib.base import ValidationException, abort, gettext +from pylons.i18n import get_lang + import ckan.lib.base as base -from pylons.i18n import get_lang, _ import ckan.lib.helpers as h import ckan.lib.maintain as maintain -from ckan.lib.navl.dictization_functions import DataError, unflatten, validate -from ckan.logic import NotFound, NotAuthorized, ValidationError -from ckan.logic import check_access, get_action -from ckan.logic import tuplize_dict, clean_dict, parse_params -import ckan.logic.action.get +import ckan.lib.navl.dictization_functions as dict_fns +import ckan.logic as logic import ckan.lib.search as search -import ckan.new_authz - -from ckan.lib.plugins import lookup_group_plugin +import ckan.model as model +import ckan.new_authz as new_authz +import ckan.lib.plugins import ckan.plugins as plugins - -try: - from collections import OrderedDict # 2.7 -except ImportError: - from sqlalchemy.util import OrderedDict +from ckan.common import OrderedDict, c, g, request, _ log = logging.getLogger(__name__) +render = base.render +abort = base.abort + +NotFound = logic.NotFound +NotAuthorized = logic.NotAuthorized +ValidationError = logic.ValidationError +check_access = logic.check_access +get_action = logic.get_action +tuplize_dict = logic.tuplize_dict +clean_dict = logic.clean_dict +parse_params = logic.parse_params + +lookup_group_plugin = ckan.lib.plugins.lookup_group_plugin + -class GroupController(BaseController): +class GroupController(base.BaseController): group_type = 'group' @@ -48,7 +54,7 @@ def _db_to_form_schema(self, group_type=None): def _setup_template_variables(self, context, data_dict, group_type=None): return lookup_group_plugin(group_type).\ - setup_template_variables(context, data_dict) + setup_template_variables(context, data_dict) def _new_template(self, group_type): return lookup_group_plugin(group_type).new_template() @@ -77,7 +83,6 @@ def _admins_template(self, group_type): def _bulk_process_template(self, group_type): return lookup_group_plugin(group_type).bulk_process_template() - ## end hooks def _replace_group_org(self, string): ''' substitute organization for group if this is an org''' @@ -134,8 +139,7 @@ def index(self): 'with_private': False} q = c.q = request.params.get('q', '') - data_dict = {'all_fields': True, - 'q': q,} + data_dict = {'all_fields': True, 'q': q} sort_by = c.sort_by_selected = request.params.get('sort') if sort_by: data_dict['sort'] = sort_by @@ -199,7 +203,7 @@ def _read(self, id, limit): try: description_formatted = ckan.misc.MarkdownFormat().to_html( - c.group_dict.get('description', '')) + c.group_dict.get('description', '')) c.description_formatted = genshi.HTML(description_formatted) except Exception, e: error_msg = "%s" %\ @@ -210,7 +214,7 @@ def _read(self, id, limit): # c.group_admins is used by CKAN's legacy (Genshi) templates only, # if we drop support for those then we can delete this line. - c.group_admins = ckan.new_authz.get_group_or_org_admin_ids(c.group.id) + c.group_admins = new_authz.get_group_or_org_admin_ids(c.group.id) try: page = int(request.params.get('page', 1)) @@ -234,8 +238,7 @@ def search_url(params): action='read', id=id) else: - url = self._url_for(controller='group', action='read', - id=id) + url = self._url_for(controller='group', action='read', id=id) params = [(k, v.encode('utf-8') if isinstance(v, basestring) else str(v)) for k, v in params] return url + u'?' + urlencode(params) @@ -250,8 +253,8 @@ def drill_down_url(**by): def remove_field(key, value=None, replace=None): return h.remove_url_param(key, value=value, replace=replace, - controller='group', action='read', - extras=dict(id=c.group_dict.get('name'))) + controller='group', action='read', + extras=dict(id=c.group_dict.get('name'))) c.remove_field = remove_field @@ -283,9 +286,9 @@ def pager_url(q=None, page=None): facets = OrderedDict() default_facet_titles = {'groups': _('Groups'), - 'tags': _('Tags'), - 'res_format': _('Formats'), - 'license': _('Licence'), } + 'tags': _('Tags'), + 'res_format': _('Formats'), + 'license': _('Licence')} for facet in g.facets: if facet in default_facet_titles: @@ -302,7 +305,8 @@ def pager_url(q=None, page=None): facets = plugin.group_facets( facets, self.group_type, None) - if 'capacity' in facets and (self.group_type != 'organization' or not user_member_of_orgs): + if 'capacity' in facets and (self.group_type != 'organization' or + not user_member_of_orgs): del facets['capacity'] c.facet_titles = facets @@ -328,9 +332,8 @@ def pager_url(q=None, page=None): ) c.facets = query['facets'] - maintain.deprecate_context_item( - 'facets', - 'Use `c.search_facets` instead.') + maintain.deprecate_context_item('facets', + 'Use `c.search_facets` instead.') c.search_facets = query['search_facets'] c.search_facets_limits = {} @@ -347,15 +350,13 @@ def pager_url(q=None, page=None): c.facets = {} c.page = h.Page(collection=[]) - - def bulk_process(self, id): ''' Allow bulk processing of datasets for an organization. Make private/public or delete. For organization admins.''' group_type = self._get_group_type(id.split('@')[0]) - if group_type != 'organization': + if group_type != 'organization': # FIXME: better error raise Exception('Must be an organization') @@ -385,7 +386,8 @@ def bulk_process(self, id): c.packages = c.page.items return render(self._bulk_process_template(group_type)) - # process the action first find the datasets to perform the action on. they are prefixed by dataset_ in the form data + # process the action first find the datasets to perform the action on. + # they are prefixed by dataset_ in the form data datasets = [] for param in request.params: if param.startswith('dataset_'): @@ -403,8 +405,9 @@ def bulk_process(self, id): get_action(action_functions[action])(context, data_dict) except NotAuthorized: abort(401, _('Not authorized to perform bulk update')) - base.redirect(h.url_for(controller='organization', action='bulk_process', - id=id)) + base.redirect(h.url_for(controller='organization', + action='bulk_process', + id=id)) def new(self, data=None, errors=None, error_summary=None): group_type = self._guess_group_type(True) @@ -486,7 +489,7 @@ def _get_group_type(self, id): def _save_new(self, context, group_type=None): try: - data_dict = clean_dict(unflatten( + data_dict = clean_dict(dict_fns.unflatten( tuplize_dict(parse_params(request.params)))) data_dict['type'] = group_type or 'group' context['message'] = data_dict.get('log_message', '') @@ -499,7 +502,7 @@ def _save_new(self, context, group_type=None): abort(401, _('Unauthorized to read group %s') % '') except NotFound, e: abort(404, _('Group not found')) - except DataError: + except dict_fns.DataError: abort(400, _(u'Integrity Error')) except ValidationError, e: errors = e.error_dict @@ -517,7 +520,7 @@ def _force_reindex(self, grp): def _save_edit(self, id, context): try: - data_dict = clean_dict(unflatten( + data_dict = clean_dict(dict_fns.unflatten( tuplize_dict(parse_params(request.params)))) context['message'] = data_dict.get('log_message', '') data_dict['id'] = id @@ -532,7 +535,7 @@ def _save_edit(self, id, context): abort(401, _('Unauthorized to read group %s') % id) except NotFound, e: abort(404, _('Group not found')) - except DataError: + except dict_fns.DataError: abort(400, _(u'Integrity Error')) except ValidationError, e: errors = e.error_dict @@ -556,8 +559,8 @@ def authz(self, id): c.authz_editable = False if not c.authz_editable: abort(401, - gettext('User %r not authorized to edit %s authorizations') % - (c.user, id)) + _('User %r not authorized to edit %s authorizations') % + (c.user, id)) roles = self._handle_update_of_authz(group) self._prepare_authz_info_for_render(roles) @@ -595,8 +598,9 @@ def members(self, id): 'user': c.user or c.author} try: - c.members = self._action('member_list')(context, {'id': id, - 'object_type': 'user'}) + c.members = self._action('member_list')( + context, {'id': id, 'object_type': 'user'} + ) c.group_dict = self._action('group_show')(context, {'id': id}) except NotAuthorized: abort(401, _('Unauthorized to delete group %s') % '') @@ -611,7 +615,7 @@ def member_new(self, id): #self._check_access('group_delete', context, {'id': id}) try: if request.method == 'POST': - data_dict = clean_dict(unflatten( + data_dict = clean_dict(dict_fns.unflatten( tuplize_dict(parse_params(request.params)))) data_dict['id'] = id c.group_dict = self._action('group_member_create')(context, data_dict) @@ -620,7 +624,7 @@ def member_new(self, id): user = request.params.get('user') if user: c.user_dict = get_action('user_show')(context, {'id': user}) - c.user_role = ckan.new_authz.users_role_for_group_or_org(id, user) or 'member' + c.user_role = new_authz.users_role_for_group_or_org(id, user) or 'member' else: c.user_role = 'member' c.group_dict = self._action('group_show')(context, {'id': id}) @@ -658,7 +662,6 @@ def member_delete(self, id): abort(404, _('Group not found')) return self._render_template('group/confirm_delete_member.html') - def history(self, id): if 'diff' in request.params or 'selected1' in request.params: try: @@ -682,7 +685,7 @@ def history(self, id): try: c.group_dict = self._action('group_show')(context, data_dict) c.group_revisions = self._action('group_revision_list')(context, - data_dict) + data_dict) #TODO: remove # Still necessary for the authz check in group/layout.html c.group = context['group'] @@ -698,7 +701,7 @@ def history(self, id): feed = Atom1Feed( title=_(u'CKAN Group Revision History'), link=self._url_for(controller='group', action='read', - id=c.group_dict['name']), + id=c.group_dict['name']), description=_(u'Recent changes to CKAN Group: ') + c.group_dict['display_name'], language=unicode(get_lang()), @@ -755,7 +758,7 @@ def activity(self, id, offset=0): # Add the group's activity stream (already rendered to HTML) to the # template context for the group/read.html template to retrieve later. c.group_activity_stream = get_action('group_activity_list_html')( - context, {'id': c.group_dict['id'], 'offset': offset}) + context, {'id': c.group_dict['id'], 'offset': offset}) #return render('group/activity_stream.html') return render(self._activity_template(c.group_dict['type'])) @@ -773,7 +776,7 @@ def follow(self, id): group_dict['title'])) except ValidationError as e: error_message = (e.extra_msg or e.message or e.error_summary - or e.error_dict) + or e.error_dict) h.flash_error(error_message) except NotAuthorized as e: h.flash_error(e.extra_msg) @@ -792,7 +795,7 @@ def unfollow(self, id): group_dict['title'])) except ValidationError as e: error_message = (e.extra_msg or e.message or e.error_summary - or e.error_dict) + or e.error_dict) h.flash_error(error_message) except (NotFound, NotAuthorized) as e: error_message = e.extra_msg or e.message @@ -804,15 +807,14 @@ def followers(self, id): 'user': c.user or c.author} c.group_dict = self._get_group_dict(id) try: - c.followers = get_action('group_follower_list')(context, - {'id': id}) + c.followers = get_action('group_follower_list')(context, {'id': id}) except NotAuthorized: abort(401, _('Unauthorized to view followers %s') % '') return render('group/followers.html') def admins(self, id): c.group_dict = self._get_group_dict(id) - c.admins = ckan.new_authz.get_group_or_org_admin_ids(id) + c.admins = new_authz.get_group_or_org_admin_ids(id) return render(self._admins_template(c.group_dict['type'])) def about(self, id): @@ -845,7 +847,7 @@ def _update(self, fs, group_name, group_id): validation = fs.validate() if not validation: c.form = self._render_edit_form(fs) - raise ValidationException(fs) + raise base.ValidationException(fs) try: fs.sync() @@ -859,7 +861,7 @@ def _update_authz(self, fs): validation = fs.validate() if not validation: c.form = self._render_edit_form(fs) - raise ValidationException(fs) + raise base.ValidationException(fs) try: fs.sync() except Exception, inst: diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index cb7b0c19b77..00cc0b09a37 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -3,46 +3,43 @@ import datetime from pylons import config -from pylons.i18n import _ from genshi.template import MarkupTemplate from genshi.template.text import NewTextTemplate from paste.deploy.converters import asbool -from ckan.logic import get_action, check_access -from ckan.lib.base import (request, - render, - BaseController, - model, - abort, g, c) -from ckan.lib.base import response, redirect, gettext +import ckan.logic as logic +import ckan.lib.base as base import ckan.lib.maintain as maintain -from ckan.lib.package_saver import PackageSaver, ValidationException -from ckan.lib.navl.dictization_functions import DataError, unflatten, validate -from ckan.logic import NotFound, NotAuthorized, ValidationError -from ckan.logic import (tuplize_dict, - clean_dict, - parse_params, - flatten_to_string_key) -from ckan.lib.i18n import get_lang -import ckan.rating -import ckan.misc +import ckan.lib.package_saver as package_saver +import ckan.lib.i18n as i18n +import ckan.lib.navl.dictization_functions as dict_fns import ckan.lib.accept as accept import ckan.lib.helpers as h +import ckan.model as model import ckan.lib.datapreview as datapreview -import ckan.plugins as plugins -from home import CACHE_PARAMETERS - -from ckan.lib.plugins import lookup_package_plugin +import ckan.lib.plugins import ckan.plugins as p - -try: - from collections import OrderedDict # 2.7 -except ImportError: - from sqlalchemy.util import OrderedDict +from ckan.common import OrderedDict, _, json, request, c, g, response +from home import CACHE_PARAMETERS log = logging.getLogger(__name__) +render = base.render +abort = base.abort +redirect = base.redirect + +NotFound = logic.NotFound +NotAuthorized = logic.NotAuthorized +ValidationError = logic.ValidationError +check_access = logic.check_access +get_action = logic.get_action +tuplize_dict = logic.tuplize_dict +clean_dict = logic.clean_dict +parse_params = logic.parse_params +flatten_to_string_key = logic.flatten_to_string_key + +lookup_package_plugin = ckan.lib.plugins.lookup_package_plugin def _encode_params(params): return [(k, v.encode('utf-8') if isinstance(v, basestring) else str(v)) @@ -62,24 +59,11 @@ def search_url(params, package_type=None): return url_with_params(url, params) -class PackageController(BaseController): +class PackageController(base.BaseController): def _package_form(self, package_type=None): return lookup_package_plugin(package_type).package_form() - def _form_to_db_schema(self, package_type=None): - return lookup_package_plugin(package_type).form_to_db_schema() - - def _db_to_form_schema(self, package_type=None): - '''This is an interface to manipulate data from the database - into a format suitable for the form (optional)''' - return lookup_package_plugin(package_type).db_to_form_schema() - - def _check_data_dict(self, data_dict, package_type=None): - '''Check if the return data is correct, mostly for checking out if - spammers are submitting only part of the form''' - return lookup_package_plugin(package_type).check_data_dict(data_dict) - def _setup_template_variables(self, context, data_dict, package_type=None): return lookup_package_plugin(package_type).\ setup_template_variables(context, data_dict) @@ -245,7 +229,7 @@ def pager_url(q=None, page=None): facets[facet] = facet # Facet titles - for plugin in plugins.PluginImplementations(plugins.IFacets): + for plugin in p.PluginImplementations(p.IFacets): facets = plugin.dataset_facets(facets, package_type) c.facet_titles = facets @@ -363,7 +347,7 @@ def read(self, id, format='html'): self._setup_template_variables(context, {'id': id}, package_type=package_type) - PackageSaver().render_package(c.pkg_dict, context) + package_saver.PackageSaver().render_package(c.pkg_dict, context) template = self._read_template(package_type) template = template[:template.index('.') + 1] + format @@ -388,7 +372,7 @@ def comments(self, id): c.current_package_id = c.pkg.id #render the package - PackageSaver().render_package(c.pkg_dict) + package_saver.PackageSaver().render_package(c.pkg_dict) return render(self._comments_template(package_type)) def history(self, id): @@ -435,7 +419,7 @@ def history(self, id): id=c.pkg_dict['name']), description=_(u'Recent changes to CKAN Dataset: ') + (c.pkg_dict['title'] or ''), - language=unicode(get_lang()), + language=unicode(i18n.get_lang()), ) for revision_dict in c.pkg_revisions: revision_date = h.date_str_to_datetime( @@ -489,7 +473,7 @@ def new(self, data=None, errors=None, error_summary=None): if context['save'] and not data: return self._save_new(context, package_type=package_type) - data = data or clean_dict(unflatten(tuplize_dict(parse_params( + data = data or clean_dict(dict_fns.unflatten(tuplize_dict(parse_params( request.params, ignore_keys=CACHE_PARAMETERS)))) c.resources_json = h.json.dumps(data.get('resources', [])) # convert tags if not supplied in data @@ -533,7 +517,7 @@ def new(self, data=None, errors=None, error_summary=None): def resource_edit(self, id, resource_id, data=None, errors=None, error_summary=None): if request.method == 'POST' and not data: - data = data or clean_dict(unflatten(tuplize_dict(parse_params( + data = data or clean_dict(dict_fns.unflatten(tuplize_dict(parse_params( request.POST)))) # we don't want to include save as it is part of the form del data['save'] @@ -598,7 +582,7 @@ def new_resource(self, id, data=None, errors=None, error_summary=None): forms. ''' if request.method == 'POST' and not data: save_action = request.params.get('save') - data = data or clean_dict(unflatten(tuplize_dict(parse_params( + data = data or clean_dict(dict_fns.unflatten(tuplize_dict(parse_params( request.POST)))) # we don't want to include save as it is part of the form del data['save'] @@ -688,7 +672,7 @@ def new_metadata(self, id, data=None, errors=None, error_summary=None): if request.method == 'POST' and not data: save_action = request.params.get('save') - data = data or clean_dict(unflatten(tuplize_dict(parse_params( + data = data or clean_dict(dict_fns.unflatten(tuplize_dict(parse_params( request.POST)))) # we don't want to include save as it is part of the form del data['save'] @@ -809,22 +793,14 @@ def read_ajax(self, id, revision=None): package_type = self._get_package_type(id) context = {'model': model, 'session': model.Session, 'user': c.user or c.author, - 'schema': self._form_to_db_schema( - package_type=package_type), 'revision_id': revision} try: data = get_action('package_show')(context, {'id': id}) - schema = self._db_to_form_schema(package_type=package_type) - if schema: - data, errors = validate(data, schema) except NotAuthorized: abort(401, _('Unauthorized to read package %s') % '') except NotFound: abort(404, _('Dataset not found')) - ## hack as db_to_form schema should have this - data['tag_string'] = ', '.join([tag['name'] for tag - in data.get('tags', [])]) data.pop('tags') data = flatten_to_string_key(data) response.headers['Content-Type'] = 'application/json;charset=utf-8' @@ -889,12 +865,9 @@ def _save_new(self, context, package_type=None): # this is a real new. is_an_update = False ckan_phase = request.params.get('_ckan_phase') - if ckan_phase: - # phased add dataset so use api schema for validation - context['api_version'] = 3 from ckan.lib.search import SearchIndexError try: - data_dict = clean_dict(unflatten( + data_dict = clean_dict(dict_fns.unflatten( tuplize_dict(parse_params(request.POST)))) if ckan_phase: # prevent clearing of groups etc @@ -943,7 +916,7 @@ def _save_new(self, context, package_type=None): abort(401, _('Unauthorized to read package %s') % '') except NotFound, e: abort(404, _('Dataset not found')) - except DataError: + except dict_fns.DataError: abort(400, _(u'Integrity Error')) except SearchIndexError, e: try: @@ -969,7 +942,7 @@ def _save_edit(self, name_or_id, context, package_type=None): log.debug('Package save request name: %s POST: %r', name_or_id, request.POST) try: - data_dict = clean_dict(unflatten( + data_dict = clean_dict(dict_fns.unflatten( tuplize_dict(parse_params(request.POST)))) if '_ckan_phase' in data_dict: # we allow partial updates to not destroy existing resources @@ -994,7 +967,7 @@ def _save_edit(self, name_or_id, context, package_type=None): abort(401, _('Unauthorized to read package %s') % id) except NotFound, e: abort(404, _('Dataset not found')) - except DataError: + except dict_fns.DataError: abort(400, _(u'Integrity Error')) except SearchIndexError, e: try: @@ -1039,7 +1012,7 @@ def _adjust_license_id_options(self, pkg, fs): def authz(self, id): pkg = model.Package.get(id) if pkg is None: - abort(404, gettext('Dataset not found')) + abort(404, _('Dataset not found')) # needed to add in the tab bar to the top of the auth page c.pkg = pkg c.pkgname = pkg.name @@ -1053,7 +1026,7 @@ def authz(self, id): except NotAuthorized: c.authz_editable = False if not c.authz_editable: - abort(401, gettext('User %r not authorized to edit %s ' + abort(401, _('User %r not authorized to edit %s ' 'authorizations') % (c.user, id)) roles = self._handle_update_of_authz(pkg) @@ -1166,7 +1139,7 @@ def _update_authz(self, fs): validation = fs.validate() if not validation: c.form = self._render_edit_form(fs, request.params) - raise ValidationException(fs) + raise package_saver.ValidationException(fs) try: fs.sync() except Exception, inst: @@ -1414,7 +1387,7 @@ def resource_datapreview(self, id, resource_id): plugin = plugins_that_can_preview[0] plugin.setup_template_variables(context, data_dict) - c.resource_json = h.json.dumps(c.resource) + c.resource_json = json.dumps(c.resource) except NotFound: abort(404, _('Resource not found')) diff --git a/ckan/controllers/tag.py b/ckan/controllers/tag.py index bc45347db75..a736c17e8d0 100644 --- a/ckan/controllers/tag.py +++ b/ckan/controllers/tag.py @@ -1,5 +1,5 @@ from pylons.i18n import _ -from pylons import request, c +from pylons import request, c, config import ckan.logic as logic import ckan.model as model @@ -65,4 +65,7 @@ def read(self, id): except logic.NotFound: base.abort(404, _('Tag not found')) - return base.render('tag/read.html') + if h.asbool(config.get('ckan.legacy_templates', False)): + return base.render('tag/read.html') + else: + h.redirect_to(controller='package', action='search', tags=c.tag.get('name')) diff --git a/ckan/lib/activity_streams_session_extension.py b/ckan/lib/activity_streams_session_extension.py index 08a6e37756c..e1945657462 100644 --- a/ckan/lib/activity_streams_session_extension.py +++ b/ckan/lib/activity_streams_session_extension.py @@ -1,7 +1,11 @@ +from pylons import config from sqlalchemy.orm.session import SessionExtension +from paste.deploy.converters import asbool import logging + logger = logging.getLogger(__name__) + def activity_stream_item(obj, activity_type, revision, user_id): method = getattr(obj, "activity_stream_item", None) if callable(method): @@ -11,6 +15,7 @@ def activity_stream_item(obj, activity_type, revision, user_id): "activity_stream_item() method, it must not be a package.") return None + def activity_stream_detail(obj, activity_id, activity_type): method = getattr(obj, "activity_stream_detail", None) @@ -21,6 +26,7 @@ def activity_stream_detail(obj, activity_id, activity_type): "activity_stream_detail() method.") return None + class DatasetActivitySessionExtension(SessionExtension): """Session extension that emits activity stream activities for packages and related objects. @@ -36,6 +42,8 @@ class DatasetActivitySessionExtension(SessionExtension): """ def before_commit(self, session): + if not asbool(config.get('ckan.activity_streams_enabled', 'true')): + return session.flush() diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index d6ed92f4089..48dab3d58ec 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -323,38 +323,57 @@ def _create_link_text(text, **kwargs): ) -def nav_link(text, controller, **kwargs): +def nav_link(text, *args, **kwargs): ''' params class_: pass extra class(s) to add to the tag icon: name of ckan icon to use within the link condition: if False then no link is returned ''' - if kwargs.pop('condition', True): + if len(args) > 1: + raise Exception('Too many unnamed parameters supplied') + if args: kwargs['controller'] = controller - link = _link_to(text, **kwargs) + log.warning('h.nav_link() please supply controller as a named ' + 'parameter not a positional one') + named_route = kwargs.pop('named_route', '') + if kwargs.pop('condition', True): + if named_route: + link = _link_to(text, named_route, **kwargs) + else: + link = _link_to(text, **kwargs) else: link = '' return link -def nav_named_link(text, name, **kwargs): - '''Create a link for a named route.''' - return _link_to(text, name, **kwargs) +@maintain.deprecated('h.nav_named_link is deprecated please ' + 'use h.nav_link\nNOTE: you will need to pass the ' + 'route_name as a named parameter') +def nav_named_link(text, named_route, **kwargs): + '''Create a link for a named route. + Deprecated in ckan 2.0 ''' + return nav_link(text, named_route=named_route, **kwargs) +@maintain.deprecated('h.subnav_link is deprecated please ' + 'use h.nav_link\nNOTE: if action is passed as the second ' + 'parameter make sure it is passed as a named parameter ' + 'eg. `action=\'my_action\'') def subnav_link(text, action, **kwargs): - '''Create a link for a named route.''' + '''Create a link for a named route. + Deprecated in ckan 2.0 ''' kwargs['action'] = action - return _link_to(text, **kwargs) + return nav_link(text, **kwargs) @maintain.deprecated('h.subnav_named_route is deprecated please ' - 'use h.nav_named_link') -def subnav_named_route(text, routename, **kwargs): + 'use h.nav_link\nNOTE: you will need to pass the ' + 'route_name as a named parameter') +def subnav_named_route(text, named_route, **kwargs): '''Generate a subnav element based on a named route Deprecated in ckan 2.0 ''' - return nav_named_link(text, routename, **kwargs) + return nav_link(text, named_route=named_route, **kwargs) def build_nav_main(*args): @@ -433,7 +452,7 @@ def _make_menu_item(menu_item, title, **kw): if need not in kw: raise Exception('menu item `%s` need parameter `%s`' % (menu_item, need)) - link = nav_named_link(title, menu_item, suppress_active_class=True, **item) + link = _link_to(title, menu_item, suppress_active_class=True, **item) if active: return literal('
  • ') + link + literal('
  • ') return literal('
  • ') + link + literal('
  • ') diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py index f71dd75f04d..5f643c0e26d 100644 --- a/ckan/lib/plugins.py +++ b/ckan/lib/plugins.py @@ -158,133 +158,30 @@ def register_group_plugins(map): class DefaultDatasetForm(object): - """ - Provides a default implementation of the pluggable package - controller behaviour. + '''The default implementation of IDatasetForm. - This class has 2 purposes: + See ckan.plugins.interfaces.IDatasetForm. - - it provides a base class for IDatasetForm implementations to use - if only a subset of the 5 method hooks need to be customised. + This class has two purposes: - - it provides the fallback behaviour if no plugin is setup to - provide the fallback behaviour. + 1. It provides a base class for IDatasetForm implementations to inherit + from. + + 2. It is used as the default fallback plugin, if no IDatasetForm plugin + registers itself as the fallback. Note - this isn't a plugin implementation. This is deliberate, as we don't want this being registered. - """ - def new_template(self): - """ - Returns a string representing the location of the template to be - rendered for the new page - """ - return 'package/new.html' - - def edit_template(self): - """ - Returns a string representing the location of the template to be - rendered for the edit page - """ - return 'package/edit.html' - - def comments_template(self): - """ - Returns a string representing the location of the template to be - rendered for the comments page - """ - return 'package/comments.html' - - def search_template(self): - """ - Returns a string representing the location of the template to be - rendered for the search page (if present) - """ - return 'package/search.html' - - def read_template(self): - """ - Returns a string representing the location of the template to be - rendered for the read page - """ - return 'package/read.html' - - def history_template(self): - """ - Returns a string representing the location of the template to be - rendered for the history page - """ - return 'package/history.html' - - def package_form(self): - return 'package/new_package_form.html' - - def form_to_db_schema_options(self, options): - ''' This allows us to select different schemas for different - purpose eg via the web interface or via the api or creation vs - updating. It is optional and if not available form_to_db_schema - should be used. - If a context is provided, and it contains a schema, it will be - returned. - ''' - schema = options.get('context', {}).get('schema', None) - if schema: - return schema - if options.get('api'): - if options.get('type') == 'create': - return self.form_to_db_schema_api_create() - else: - assert options.get('type') == 'update' - return self.form_to_db_schema_api_update() - else: - return self.form_to_db_schema() - - def form_to_db_schema(self): - return logic.schema.form_to_db_package_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 - into a format suitable for the form (optional)''' - return logic.schema.db_to_form_package_schema() + ''' + def create_package_schema(self): + return ckan.logic.schema.default_create_package_schema() - def db_to_form_schema_options(self, options): - '''This allows the selectino of different schemas for different - purposes. It is optional and if not available, ``db_to_form_schema`` - should be used. - If a context is provided, and it contains a schema, it will be - returned. - ''' - schema = options.get('context', {}).get('schema', None) - if schema: - return schema - return self.db_to_form_schema() + def update_package_schema(self): + return ckan.logic.schema.default_update_package_schema() - def check_data_dict(self, data_dict, schema=None): - '''Check if the return data is correct, mostly for checking out - if spammers are submitting only part of the form''' - - # Resources might not exist yet (eg. Add Dataset) - surplus_keys_schema = ['__extras', '__junk', 'state', 'groups', - 'extras_validation', 'save', 'return_to', - 'resources', 'type', 'owner_org', 'private', - 'log_message', 'tag_string', 'tags', - 'url', 'version', 'extras'] - - if not schema: - schema = self.form_to_db_schema() - schema_keys = schema.keys() - keys_in_schema = set(schema_keys) - set(surplus_keys_schema) - - missing_keys = keys_in_schema - set(data_dict.keys()) - if missing_keys: - log.info('incorrect form fields posted, missing %s' % missing_keys) - raise dictization_functions.DataError(data_dict) + def show_package_schema(self): + return ckan.logic.schema.default_show_package_schema() def setup_template_variables(self, context, data_dict): authz_fn = logic.get_action('group_list_authz') @@ -311,6 +208,27 @@ def setup_template_variables(self, context, data_dict): except logic.NotAuthorized: c.auth_for_change_state = False + def new_template(self): + return 'package/new.html' + + def read_template(self): + return 'package/read.html' + + def edit_template(self): + return 'package/edit.html' + + def comments_template(self): + return 'package/comments.html' + + def search_template(self): + return 'package/search.html' + + def history_template(self): + return 'package/history.html' + + def package_form(self): + return 'package/new_package_form.html' + class DefaultGroupForm(object): """ diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index c1ece99aa19..df259500790 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -1,5 +1,7 @@ import logging +from pylons import config from pylons.i18n import _ +from paste.deploy.converters import asbool import ckan.new_authz as new_authz import ckan.lib.plugins as lib_plugins @@ -107,22 +109,22 @@ def package_create(context, data_dict): package_type = data_dict.get('type') package_plugin = lib_plugins.lookup_package_plugin(package_type) - try: - schema = package_plugin.form_to_db_schema_options({'type':'create', - 'api':'api_version' in context, - 'context': context}) - except AttributeError: - schema = package_plugin.form_to_db_schema() + schema = package_plugin.create_package_schema() _check_access('package_create', context, data_dict) if 'api_version' not in context: - # old plugins do not support passing the schema so we need - # to ensure they still work - try: - package_plugin.check_data_dict(data_dict, schema) - except TypeError: - package_plugin.check_data_dict(data_dict) + # check_data_dict() is deprecated. If the package_plugin has a + # check_data_dict() we'll call it, if it doesn't have the method we'll + # do nothing. + check_data_dict = getattr(package_plugin, 'check_datadict', None) + if check_data_dict: + try: + check_data_dict(data_dict, schema) + except TypeError: + # Old plugins do not support passing the schema so we need + # to ensure they still work + package_plugin.check_data_dict(data_dict) data, errors = _validate(data_dict, schema, context) log.debug('package_create validate_errs=%r user=%s package=%s data=%r', @@ -178,19 +180,6 @@ def package_create(context, data_dict): return output -def package_create_validate(context, data_dict): - model = context['model'] - schema = lib_plugins.lookup_package_plugin().form_to_db_schema() - - _check_access('package_create',context,data_dict) - - data, errors = _validate(data_dict, schema, context) - if errors: - model.Session.rollback() - raise ValidationError(errors) - else: - return data - def resource_create(context, data_dict): '''Appends a new resource to a datasets list of resources. @@ -435,13 +424,14 @@ def member_create(context, data_dict=None): filter(model.Member.table_name == obj_type).\ filter(model.Member.table_id == obj_id).\ filter(model.Member.group_id == group.id).\ - filter(model.Member.state == "active").first() + filter(model.Member.state == "active").first() if member: member.capacity = capacity else: member = model.Member(table_name = obj_type, table_id = obj_id, group_id = group.id, + state = 'active', capacity=capacity) model.Session.add(member) @@ -901,6 +891,9 @@ def activity_create(context, activity_dict, ignore_auth=False): :rtype: dictionary ''' + if not asbool(config.get('ckan.activity_streams_enabled', 'true')): + return + model = context['model'] # Any revision_id that the caller attempts to pass in the activity_dict is diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 19c18aad479..45417d8f8a6 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -11,6 +11,7 @@ import ckan.lib.dictization import ckan.logic as logic import ckan.logic.action +import ckan.logic.schema import ckan.lib.dictization.model_dictize as model_dictize import ckan.lib.navl.dictization_functions import ckan.model.misc as misc @@ -334,9 +335,9 @@ def _group_or_org_list(context, data_dict, is_org=False): if q: q = u'%{0}%'.format(q) query = query.filter(_or_( - model.GroupRevision.name.like(q), - model.GroupRevision.title.like(q), - model.GroupRevision.description.like(q), + model.GroupRevision.name.ilike(q), + model.GroupRevision.title.ilike(q), + model.GroupRevision.description.ilike(q), )) @@ -737,13 +738,7 @@ def package_show(context, data_dict): item.read(pkg) package_plugin = lib_plugins.lookup_package_plugin(package_dict['type']) - try: - schema = package_plugin.db_to_form_schema_options({ - 'type':'show', - 'api': 'api_version' in context, - 'context': context }) - except AttributeError: - schema = package_plugin.db_to_form_schema() + schema = package_plugin.show_package_schema() if schema and context.get('validate', True): package_dict, errors = _validate(package_dict, schema, context=context) diff --git a/ckan/logic/action/update.py b/ckan/logic/action/update.py index 757d314a346..d6b969bc923 100644 --- a/ckan/logic/action/update.py +++ b/ckan/logic/action/update.py @@ -235,20 +235,20 @@ def package_update(context, data_dict): # get the schema package_plugin = lib_plugins.lookup_package_plugin(pkg.type) - try: - schema = package_plugin.form_to_db_schema_options({'type':'update', - 'api':'api_version' in context, - 'context': context}) - except AttributeError: - schema = package_plugin.form_to_db_schema() + schema = package_plugin.update_package_schema() if 'api_version' not in context: - # old plugins do not support passing the schema so we need - # to ensure they still work - try: - package_plugin.check_data_dict(data_dict, schema) - except TypeError: - package_plugin.check_data_dict(data_dict) + # check_data_dict() is deprecated. If the package_plugin has a + # check_data_dict() we'll call it, if it doesn't have the method we'll + # do nothing. + check_data_dict = getattr(package_plugin, 'check_data_dict', None) + if check_data_dict: + try: + package_plugin.check_data_dict(data_dict, schema) + except TypeError: + # Old plugins do not support passing the schema so we need + # to ensure they still work. + package_plugin.check_data_dict(data_dict) data, errors = _validate(data_dict, schema, context) log.debug('package_update validate_errs=%r user=%s package=%s data=%r', @@ -295,37 +295,6 @@ def package_update(context, data_dict): return output -def package_update_validate(context, data_dict): - model = context['model'] - user = context['user'] - - id = _get_or_bust(data_dict, "id") - - pkg = model.Package.get(id) - context["package"] = pkg - - if pkg is None: - raise NotFound(_('Package was not found.')) - data_dict["id"] = pkg.id - - # get the schema - package_plugin = lib_plugins.lookup_package_plugin(pkg.type) - try: - schema = package_plugin.form_to_db_schema_options({'type':'update', - 'api':'api_version' in context, - 'context': context}) - except AttributeError: - schema = package_plugin.form_to_db_schema() - - _check_access('package_update', context, data_dict) - - data, errors = _validate(data_dict, schema, context) - if errors: - model.Session.rollback() - raise ValidationError(errors) - return data - - def _update_package_relationship(relationship, comment, context): model = context['model'] api = context.get('api_version') diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index c79af5bea3a..07b22b6f9cb 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -50,6 +50,7 @@ convert_group_name_or_id_to_id,) from formencode.validators import OneOf import ckan.model +import ckan.lib.maintain as maintain def default_resource_schema(): @@ -112,10 +113,10 @@ def default_create_tag_schema(): schema['id'] = [empty] return schema -def default_package_schema(): +def default_create_package_schema(): schema = { - 'id': [ignore_missing, unicode, package_id_exists], + 'id': [empty], 'revision_id': [ignore], 'name': [not_empty, unicode, name_validator, package_name_validator], 'title': [if_empty_same_as("name"), unicode], @@ -130,12 +131,17 @@ def default_package_schema(): 'state': [ignore_not_package_admin, ignore_missing], 'type': [ignore_missing, unicode], 'owner_org': [owner_org_validator, unicode], + 'log_message': [ignore_missing, unicode, no_http], 'private': [ignore_missing, boolean_validator], '__extras': [ignore], '__junk': [empty], 'resources': default_resource_schema(), 'tags': default_tags_schema(), + 'tag_string': [ignore_missing, tag_string_convert], 'extras': default_extras_schema(), + 'extras_validation': [duplicate_extras_key, ignore], + 'save': [ignore], + 'return_to': [ignore], 'relationships_as_object': default_relationship_schema(), 'relationships_as_subject': default_relationship_schema(), 'groups': { @@ -147,56 +153,31 @@ def default_package_schema(): } return schema -def default_create_package_schema(): - - schema = default_package_schema() - schema["id"] = [empty] - - return schema - def default_update_package_schema(): + schema = default_create_package_schema() - schema = default_package_schema() - schema["id"] = [ignore_missing, package_id_not_changed] - schema["name"] = [ignore_missing, name_validator, package_name_validator, unicode] - schema["title"] = [ignore_missing, unicode] - - schema['private'] = [ignore_missing, boolean_validator] - schema['owner_org'] = [ignore_missing, owner_org_validator, unicode] - return schema + # Users can (optionally) supply the package id when updating a package, but + # only to identify the package to be updated, they cannot change the id. + schema['id'] = [ignore_missing, package_id_not_changed] -def package_form_schema(): - # This function is deprecated and was replaced by - # form_to_db_package_schema(), it remains here for backwards compatibility. - return form_to_db_package_schema() + # Supplying the package name when updating a package is optional (you can + # supply the id to identify the package instead). + schema['name'] = [ignore_missing, name_validator, package_name_validator, + unicode] -def form_to_db_package_schema(): + # Supplying the package title when updating a package is optional, if it's + # not supplied the title will not be changed. + schema['title'] = [ignore_missing, unicode] - schema = default_package_schema() - ##new - schema['log_message'] = [ignore_missing, unicode, no_http] - schema['groups'] = { - 'id': [ignore_missing, unicode], - '__extras': [ignore], - } - schema['tag_string'] = [ignore_missing, tag_string_convert] - schema['extras_validation'] = [duplicate_extras_key, ignore] - schema['save'] = [ignore] - schema['return_to'] = [ignore] - schema['type'] = [ignore_missing, unicode] - schema['private'] = [ignore_missing, boolean_validator] schema['owner_org'] = [ignore_missing, owner_org_validator, unicode] - ##changes - schema.pop("id") - schema.pop('relationships_as_object') - schema.pop('revision_id') - schema.pop('relationships_as_subject') - return schema -def db_to_form_package_schema(): - schema = default_package_schema() +def default_show_package_schema(): + schema = default_create_package_schema() + + # Don't strip ids from package dicts when validating them. + schema['id'] = [] schema.update({ 'tags': {'__extras': [ckan.lib.navl.validators.keep_extras]}}) @@ -246,7 +227,7 @@ def db_to_form_package_schema(): schema['url'] = [] schema['version'] = [] - # Add several keys that are missing from default_package_schema(), so + # Add several keys that are missing from default_create_package_schema(), so # validation doesn't strip the keys from the package dicts. schema['metadata_created'] = [] schema['metadata_modified'] = [] diff --git a/ckan/new_authz.py b/ckan/new_authz.py index 197d6ab35cf..94166741e5d 100644 --- a/ckan/new_authz.py +++ b/ckan/new_authz.py @@ -1,17 +1,12 @@ import sys from logging import getLogger -try: - from collections import OrderedDict # 2.7 -except ImportError: - from sqlalchemy.util import OrderedDict - -from pylons import config, c -from pylons.i18n import _ +from pylons import config from paste.deploy.converters import asbool import ckan.plugins as p import ckan.model as model +from ckan.common import OrderedDict, _, c log = getLogger(__name__) @@ -49,6 +44,7 @@ def get_group_or_org_admin_ids(group_id): q = model.Session.query(model.Member) \ .filter(model.Member.group_id == group_id) \ .filter(model.Member.table_name == 'user') \ + .filter(model.Member.state == 'active') \ .filter(model.Member.capacity == 'admin') return [a.table_id for a in q.all()] @@ -135,6 +131,7 @@ def has_user_permission_for_group_or_org(group_id, user_name, permission): q = model.Session.query(model.Member) \ .filter(model.Member.group_id == group_id) \ .filter(model.Member.table_name == 'user') \ + .filter(model.Member.state == 'active') \ .filter(model.Member.table_id == user_id) # see if any role has the required permission # admin permission allows anything for the group @@ -158,6 +155,7 @@ def users_role_for_group_or_org(group_id, user_name): q = model.Session.query(model.Member) \ .filter(model.Member.group_id == group_id) \ .filter(model.Member.table_name == 'user') \ + .filter(model.Member.state == 'active') \ .filter(model.Member.table_id == user_id) # return the first role we find for row in q.all(): @@ -176,6 +174,7 @@ def has_user_permission_for_some_org(user_name, permission): # get any groups the user has with the needed role q = model.Session.query(model.Member) \ .filter(model.Member.table_name == 'user') \ + .filter(model.Member.state == 'active') \ .filter(model.Member.capacity.in_(roles)) \ .filter(model.Member.table_id == user_id) group_ids = [] diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py index ea0a1881c78..1dd22b2f88c 100644 --- a/ckan/plugins/interfaces.py +++ b/ckan/plugins/interfaces.py @@ -525,123 +525,211 @@ def get_helpers(self): class IDatasetForm(Interface): - """ - Allows customisation of the package controller as a plugin. + '''Customize CKAN's dataset (package) schemas and forms. - The behaviour of the plugin is determined by 5 method hooks: + By implementing this interface plugins can customise CKAN's dataset schema, + for example to add new custom fields to datasets. - - package_form(self) - - form_to_db_schema(self) - - db_to_form_schema(self) - - check_data_dict(self, data_dict, schema=None) - - setup_template_variables(self, context, data_dict) + Multiple IDatasetForm plugins can be used at once, each plugin associating + itself with different package types using the ``package_types()`` and + ``is_fallback()`` methods below, and then providing different schemas and + templates for different types of dataset. When a package controller action + is invoked, the ``type`` field of the package will determine which + IDatasetForm plugin (if any) gets delegated to. - Furthermore, there can be many implementations of this plugin registered - at once. With each instance associating itself with 0 or more package - type strings. When a package controller action is invoked, the package - type determines which of the registered plugins to delegate to. Each - implementation must implement two methods which are used to determine the - package-type -> plugin mapping: + When implementing IDatasetForm, you can inherit from + ``ckan.plugins.toolkit.DefaultDatasetForm``, which provides default + implementations for each of the methods defined in this interface. - - is_fallback(self) - - package_types(self) + See ``ckanext/example_idatasetform`` for an example plugin. - Implementations might want to consider mixing in - ckan.lib.plugins.DefaultDatasetForm which provides - default behaviours for the 5 method hooks. + ''' + def package_types(self): + '''Return an iterable of package types that this plugin handles. - """ + If a request involving a package of one of the returned types is made, + then this plugin instance will be delegated to. - ##### These methods control when the plugin is delegated to ##### + There cannot be two IDatasetForm plugins that return the same package + type, if this happens then CKAN will raise an exception at startup. + + :rtype: iterable of strings + + ''' def is_fallback(self): - """ - Returns true iff this provides the fallback behaviour, when no other - plugin instance matches a package's type. + '''Return ``True`` if this plugin is the fallback plugin. - There must be exactly one fallback controller defined, any attempt to - register more than one will throw an exception at startup. If there's - no fallback registered at startup the - ckan.lib.plugins.DefaultDatasetForm is used as the fallback. - """ + When no IDatasetForm plugin's ``package_types()`` match the ``type`` of + the package being processed, the fallback plugin is delegated to + instead. - def package_types(self): - """ - Returns an iterable of package type strings. + There cannot be more than one IDatasetForm plugin whose + ``is_fallback()`` method returns ``True``, if this happens CKAN will + raise an exception at startup. - If a request involving a package of one of those types is made, then - this plugin instance will be delegated to. + If no IDatasetForm plugin's ``is_fallback()`` method returns ``True``, + CKAN will use ``DefaultDatasetForm`` as the fallback. - There must only be one plugin registered to each package type. Any - attempts to register more than one plugin instance to a given package - type will raise an exception at startup. - """ + :rtype: boolean - ##### End of control methods + ''' + + def create_package_schema(self): + '''Return the schema for validating new dataset dicts. + + CKAN will use the returned schema to validate and convert data coming + from users (via the dataset form or API) when creating new datasets, + before entering that data into the database. + + If it inherits from ``ckan.plugins.toolkit.DefaultDatasetForm``, a + plugin can call ``DefaultDatasetForm``'s ``create_package_schema()`` + method to get the default schema and then modify and return it. + + CKAN's ``convert_to_tags()`` or ``convert_to_extras()`` functions can + be used to convert custom fields into dataset tags or extras for + storing in the database. + + See ``ckanext/example_idatasetform`` for examples. + + :returns: a dictionary mapping dataset dict keys to lists of validator + and converter functions to be applied to those keys + :rtype: dictionary + + ''' + + def update_package_schema(self): + '''Return the schema for validating updated dataset dicts. + + CKAN will use the returned schema to validate and convert data coming + from users (via the dataset form or API) when updating datasets, before + entering that data into the database. + + If it inherits from ``ckan.plugins.toolkit.DefaultDatasetForm``, a + plugin can call ``DefaultDatasetForm``'s ``update_package_schema()`` + method to get the default schema and then modify and return it. + + CKAN's ``convert_to_tags()`` or ``convert_to_extras()`` functions can + be used to convert custom fields into dataset tags or extras for + storing in the database. + + See ``ckanext/example_idatasetform`` for examples. + + :returns: a dictionary mapping dataset dict keys to lists of validator + and converter functions to be applied to those keys + :rtype: dictionary + + ''' + + def show_package_schema(self): + ''' + Return a schema to validate datasets before they're shown to the user. + + CKAN will use the returned schema to validate and convert data coming + from the database before it is returned to the user via the API or + passed to a template for rendering. + + If it inherits from ``ckan.plugins.toolkit.DefaultDatasetForm``, a + plugin can call ``DefaultDatasetForm``'s ``show_package_schema()`` + method to get the default schema and then modify and return it. + + If you have used ``convert_to_tags()`` or ``convert_to_extras()`` in + your ``create_package_schema()`` and ``update_package_schema()`` then + you should use ``convert_from_tags()`` or ``convert_from_extras()`` in + your ``show_package_schema()`` to convert the tags or extras in the + database back into your custom dataset fields. + + See ``ckanext/example_idatasetform`` for examples. + + :returns: a dictionary mapping dataset dict keys to lists of validator + and converter functions to be applied to those keys + :rtype: dictionary + + ''' + + def setup_template_variables(self, context, data_dict): + '''Add variables to the template context for use in templates. + + This function is called before a dataset template is rendered. If you + have custom dataset templates that require some additional variables, + you can add them to the template context ``ckan.plugins.toolkit.c`` + here and they will be available in your templates. See + ``ckanext/example_idatasetform`` for an example. + + ''' - ##### Hooks for customising the PackageController's behaviour ##### - ##### TODO: flesh out the docstrings a little more. ##### def new_template(self): - """ - Returns a string representing the location of the template to be - rendered for the new page - """ + '''Return the path to the template for the new dataset page. + + The path should be relative to the plugin's templates dir, e.g. + ``'package/new.html'``. + + :rtype: string + + ''' + + def read_template(self): + '''Return the path to the template for the dataset read page. + + The path should be relative to the plugin's templates dir, e.g. + ``'package/read.html'``. + + :rtype: string + + ''' + + def edit_template(self): + '''Return the path to the template for the dataset edit page. + + The path should be relative to the plugin's templates dir, e.g. + ``'package/edit.html'``. + + :rtype: string + + ''' def comments_template(self): - """ - Returns a string representing the location of the template to be - rendered for the comments page - """ + '''Return the path to the template for the dataset comments page. + + The path should be relative to the plugin's templates dir, e.g. + ``'package/comments.html'``. + + :rtype: string + + ''' def search_template(self): - """ - Returns a string representing the location of the template to be - rendered for the search page (if present) - """ + '''Return the path to the template for use in the dataset search page. - def read_template(self): - """ - Returns a string representing the location of the template to be - rendered for the read page - """ + This template is used to render each dataset that is listed in the + search results on the dataset search page. + + The path should be relative to the plugin's templates dir, e.g. + ``'package/search.html'``. + + :rtype: string + + ''' def history_template(self): - """ - Returns a string representing the location of the template to be - rendered for the history page - """ + '''Return the path to the template for the dataset history page. - def package_form(self): - """ - Returns a string representing the location of the template to be - rendered. e.g. "package/new_package_form.html". - """ + The path should be relative to the plugin's templates dir, e.g. + ``'package/history.html'``. - def form_to_db_schema(self): - """ - Returns the schema for mapping package data from a form to a format - suitable for the database. - """ + :rtype: string - def db_to_form_schema(self): - """ - Returns the schema for mapping package data from the database into a - format suitable for the form (optional) - """ + ''' - def check_data_dict(self, data_dict, schema=None): - """ - Check if the return data is correct. + def package_form(self): + '''Return the path to the template for the dataset form. - raise a DataError if not. - """ + The path should be relative to the plugin's templates dir, e.g. + ``'package/form.html'``. - def setup_template_variables(self, context, data_dict): - """ - Add variables to c just prior to the template being rendered. - """ + :rtype: string - ##### End of hooks ##### + ''' class IGroupForm(Interface): diff --git a/ckan/plugins/toolkit.py b/ckan/plugins/toolkit.py index 7131d8a5442..e011184fe56 100644 --- a/ckan/plugins/toolkit.py +++ b/ckan/plugins/toolkit.py @@ -51,6 +51,7 @@ class _Toolkit(object): 'UnknownValidator', # validator not found exception 'ValidationError', # model update validation error 'CkanCommand', # class for providing cli interfaces + 'DefaultDatasetForm', # base class for IDatasetForm plugins ## Fully defined in this file ## 'add_template_directory', @@ -72,6 +73,7 @@ def _initialize(self): import ckan.lib.base as base import ckan.logic as logic import ckan.lib.cli as cli + import ckan.lib.plugins as lib_plugins # Allow class access to these modules self.__class__.ckan = ckan @@ -101,6 +103,7 @@ def _initialize(self): t['UnknownValidator'] = logic.UnknownValidator t['CkanCommand'] = cli.CkanCommand + t['DefaultDatasetForm'] = lib_plugins.DefaultDatasetForm # class functions t['render_snippet'] = self._render_snippet diff --git a/ckan/public/base/css/main.css b/ckan/public/base/css/main.css index c182477d335..a3f654b889c 100644 --- a/ckan/public/base/css/main.css +++ b/ckan/public/base/css/main.css @@ -4874,6 +4874,7 @@ a.tag:hover { margin: 20px 0; } .module-heading { + *zoom: 1; margin: 0; padding: 7px 25px; font-size: 14px; @@ -4882,6 +4883,15 @@ a.tag:hover { border-top: 1px solid #dddddd; border-bottom: 1px solid #dddddd; } +.module-heading:before, +.module-heading:after { + display: table; + content: ""; + line-height: 0; +} +.module-heading:after { + clear: both; +} .module-heading .action { float: right; color: #888888; @@ -5755,24 +5765,23 @@ textarea { background: #c6898b; margin: -3px 0 0; color: #ffffff; - width: 216px; + width: 208px; } .control-medium .error-block { - width: 326px; + width: 318px; } .control-full .error-block { width: auto; } .control-group.error .input-prepend .error-block, .control-custom.error .error-block { - margin-left: -1px; width: auto; } .control-custom.error .error-block { - width: 409px; + width: 401px; } .control-select.error .error-block { - width: 204px; + width: 196px; } .stages { margin: 0; @@ -7833,6 +7842,9 @@ textarea { .context-info.editing .module-content { margin-top: 0; } +.modal { + top: 50%; +} .hero { background: url("../../../base/images/background-tile.png"); padding: 20px 0; @@ -8185,10 +8197,15 @@ textarea { .footer-links { margin-left: 0; } -.footer-links li { - display: inline-block; - width: 44%; - margin-right: 5%; +.footer-links ul { + float: left; + margin-left: 20px; + width: 220px; +} +.footer-links ul:first-child { + margin-left: 0; +} +.footer-links ul li { margin-bottom: 5px; } .attribution small { diff --git a/ckan/public/base/less/footer.less b/ckan/public/base/less/footer.less index b91ee55ff75..34311bd64db 100644 --- a/ckan/public/base/less/footer.less +++ b/ckan/public/base/less/footer.less @@ -22,10 +22,16 @@ margin-left: 0; } -.footer-links li { - display: inline-block; - width: 44%; - margin-right: 5%; +.footer-links ul { + float: left; + .makeColumn(3); +} + +.footer-links ul:first-child { + margin-left: 0; +} + +.footer-links ul li { margin-bottom: 5px; } diff --git a/ckan/public/base/less/forms.less b/ckan/public/base/less/forms.less index 2fc4ff285f7..a7449a9980a 100644 --- a/ckan/public/base/less/forms.less +++ b/ckan/public/base/less/forms.less @@ -376,11 +376,11 @@ textarea { background: @errorBorder; margin: -@inputBorderRadius 0 0; color: @inputBackground; - width: 216px; + width: 208px; } .control-medium .error-block { - width: 326px; + width: 318px; } .control-full .error-block { @@ -389,16 +389,15 @@ textarea { .control-group.error .input-prepend .error-block, .control-custom.error .error-block { - margin-left: -1px; width: auto; } .control-custom.error .error-block { - width: 409px; + width: 401px; } .control-select.error .error-block { - width: 204px; + width: 196px; } // Stages diff --git a/ckan/public/base/less/layout.less b/ckan/public/base/less/layout.less index ad12f81dc9b..a35f83f93a5 100644 --- a/ckan/public/base/less/layout.less +++ b/ckan/public/base/less/layout.less @@ -163,3 +163,7 @@ } } } + +.modal { + top: 50%; +} diff --git a/ckan/templates/footer.html b/ckan/templates/footer.html index 3c5d429f6f3..fca1141eb46 100644 --- a/ckan/templates/footer.html +++ b/ckan/templates/footer.html @@ -1,15 +1,19 @@