diff --git a/ckan/__init__.py b/ckan/__init__.py index 9bdf93a52a7..b4333195546 100644 --- a/ckan/__init__.py +++ b/ckan/__init__.py @@ -1,4 +1,4 @@ -__version__ = '1.6.1a' +__version__ = '1.6.1b' __description__ = 'Comprehensive Knowledge Archive Network (CKAN) Software' __long_description__ = \ '''CKAN software provides a hub for datasets. The flagship site running CKAN diff --git a/ckan/config/plugins.py b/ckan/config/plugins.py deleted file mode 100644 index 645f3db7be5..00000000000 --- a/ckan/config/plugins.py +++ /dev/null @@ -1,65 +0,0 @@ -import logging -from pkg_resources import iter_entry_points - -log = logging.getLogger(__name__) - -# Entry point group. -GROUP_NAME = "ckan.plugins" - -class PluginException(Exception): pass - -def load_all(config): - plugins = config.get('ckan.plugins', '') - log.debug("Loading plugins: %s" % plugins) - for plugin in plugins.split(): - for entry_point in iter_entry_points(group=GROUP_NAME, name=plugin): - load(plugin, entry_point, config) - break - else: - raise PluginException("Plugin not found: %s" % plugin) - - -def load(name, entry_point, config): - log.debug("Plugin: %s", entry_point.dist) - entry_obj = entry_point.load()(config) - registry = config.get('ckan.plugin_registry', {}) - registry[entry_point] = entry_obj - config['ckan.plugin_registry'] = registry - return entry_obj - - -def find_methods(method_name): - """ For a given method name, find all plugins where that method exists and iterate over them. """ - from pylons import config - for k, v in config.get('ckan.plugin_registry', {}).items(): - if hasattr(v, method_name): - yield getattr(v, method_name) - else: - pass - #log.debug("%s has no method %s" % (k.name, method_name)) - - - -##### Pylons monkey-patch - -from pylons.wsgiapp import PylonsApp -import pkg_resources - -log.info("Monkey-patching Pylons to allow loading of controllers via entry point mechanism") - -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) - self.controller_classes[controller] = mycontroller - return mycontroller - - return find_controller_generic(self, controller) - -PylonsApp.find_controller = find_controller diff --git a/ckan/config/solr/schema-1.3.xml b/ckan/config/solr/schema-1.3.xml index bc22441ad7f..21cf3d7b75e 100644 --- a/ckan/config/solr/schema-1.3.xml +++ b/ckan/config/solr/schema-1.3.xml @@ -138,6 +138,11 @@ + + + + diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index b7f8efe2413..7c8332f47dd 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -36,9 +36,26 @@ 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) + def _new_template(self,group_type): + from ckan.lib.helpers import default_group_type + return lookup_group_plugin(group_type).new_template() + + def _index_template(self,group_type): + from ckan.lib.helpers import default_group_type + return lookup_group_plugin(group_type).index_template() + + def _read_template(self, group_type): + return lookup_group_plugin(group_type).read_template() + + def _history_template(self, group_type): + return lookup_group_plugin(group_type).history_template() + ## end hooks def index(self): + group_type = request.path.strip('/').split('/')[0] + if group_type == 'group': + group_type = None context = {'model': model, 'session': model.Session, 'user': c.user or c.author} @@ -58,7 +75,7 @@ def index(self): url=h.pager_url, items_per_page=20 ) - return render('group/index.html') + return render( self._index_template(group_type) ) def read(self, id): @@ -170,7 +187,7 @@ def pager_url(q=None, page=None): ckan.logic.action.get.group_activity_list_html(context, {'id': c.group_dict['id']}) - return render('group/read.html') + return render( self._read_template(c.group_dict['type']) ) def new(self, data=None, errors=None, error_summary=None): group_type = request.path.strip('/').split('/')[0] @@ -198,7 +215,7 @@ def new(self, data=None, errors=None, error_summary=None): self._setup_template_variables(context,data) c.form = render(self._group_form(group_type=group_type), extra_vars=vars) - return render('group/new.html') + return render(self._new_template(group_type)) def edit(self, id, data=None, errors=None, error_summary=None): group_type = self._get_group_type(id.split('@')[0]) @@ -383,7 +400,7 @@ def history(self, id): ) feed.content_type = 'application/atom+xml' return feed.writeString('utf-8') - return render('group/history.html') + return render( self._history_template(c.group_dict['type']) ) def _render_edit_form(self, fs): # errors arrive in c.error and fs.errors diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index ec055e6d64f..338814e51d9 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -44,7 +44,7 @@ def search_url(params): class PackageController(BaseController): - def _package_form(self, package_type=None): + def _package_form(self, package_type=None): return lookup_package_plugin(package_type).package_form() def _form_to_db_schema(self, package_type=None): @@ -63,10 +63,31 @@ def _check_data_dict(self, data_dict, package_type=None): def _setup_template_variables(self, context, data_dict, package_type=None): return lookup_package_plugin(package_type).setup_template_variables(context, data_dict) + def _new_template(self, package_type): + return lookup_package_plugin(package_type).new_template() + + def _comments_template(self, package_type): + return lookup_package_plugin(package_type).comments_template() + + def _search_template(self, package_type): + return lookup_package_plugin(package_type).search_template() + + def _read_template(self, package_type): + return lookup_package_plugin(package_type).read_template() + + def _history_template(self, package_type): + return lookup_package_plugin(package_type).history_template() + + authorizer = ckan.authz.Authorizer() def search(self): from ckan.lib.search import SearchError + + package_type = request.path.strip('/').split('/')[0] + if package_type == 'group': + package_type = None + try: context = {'model':model,'user': c.user or c.author} check_access('site_read',context) @@ -83,21 +104,45 @@ def search(self): # most search operations should reset the page counter: params_nopage = [(k, v) for k,v in request.params.items() if k != 'page'] - + def drill_down_url(**by): params = list(params_nopage) params.extend(by.items()) return search_url(set(params)) - - c.drill_down_url = drill_down_url - + + c.drill_down_url = drill_down_url + def remove_field(key, value): params = list(params_nopage) params.remove((key, value)) return search_url(params) c.remove_field = remove_field - + + sort_by = request.params.get('sort', None) + params_nosort = [(k, v) for k,v in params_nopage if k != 'sort'] + def _sort_by(fields): + """ + Sort by the given list of fields. + + Each entry in the list is a 2-tuple: (fieldname, sort_order) + + eg - [('metadata_modified', 'desc'), ('name', 'asc')] + + If fields is empty, then the default ordering is used. + """ + params = params_nosort[:] + + if fields: + sort_string = ', '.join( '%s %s' % f for f in fields ) + params.append(('sort', sort_string)) + return search_url(params) + c.sort_by = _sort_by + if sort_by is None: + c.sort_by_fields = [] + else: + c.sort_by_fields = [ field.split()[0] for field in sort_by.split(',') ] + def pager_url(q=None, page=None): params = list(params_nopage) params.append(('page', page)) @@ -108,7 +153,7 @@ def pager_url(q=None, page=None): search_extras = {} fq = '' for (param, value) in request.params.items(): - if not param in ['q', 'page'] \ + if param not in ['q', 'page', 'sort'] \ and len(value) and not param.startswith('_'): if not param.startswith('ext_'): c.fields.append((param, value)) @@ -125,6 +170,7 @@ def pager_url(q=None, page=None): 'facet.field':g.facets, 'rows':limit, 'start':(page-1)*limit, + 'sort': sort_by, 'extras':search_extras } @@ -144,17 +190,51 @@ def pager_url(q=None, page=None): c.query_error = True c.facets = {} c.page = h.Page(collection=[]) - - return render('package/search.html') + + return render( self._search_template(package_type) ) + + def _content_type_for_format(self, fmt): + """ + Given a requested format this method determines the content-type + to set and the genshi template loader to use in order to render + it accurately. TextTemplate must be used for non-xml templates + whilst all that are some sort of XML should use MarkupTemplate. + """ + from genshi.template import MarkupTemplate, TextTemplate + types = { + "html": ("text/html; charset=utf-8", MarkupTemplate), + "rdf" : ("application/rdf+xml; charset=utf-8", MarkupTemplate), + } + if fmt in types: + return types[fmt][0], fmt, types[fmt][1] + return None, "html", (types["html"][1]) def read(self, id): + # Check if the request was for a different format than html, we have to do + # it this way because if we instead rely on _content_type_for_format failing + # for revisions (with . in the name) then we will have lost the ID by virtue + # of the routing splitting it up. + format = 'html' + if '.' in id: + pos = id.index('.') + format = id[pos+1:] + id = id[:pos] + + ctype,extension,loader = self._content_type_for_format(format) + if not ctype: + # Reconstitute the ID if we don't know what content type to use + ctype = "text/html; charset=utf-8" + id = "%s.%s" % (id, format) + response.headers['Content-Type'] = ctype + package_type = self._get_package_type(id.split('@')[0]) context = {'model': model, 'session': model.Session, 'user': c.user or c.author, 'extras_as_string': True, 'for_view': True} data_dict = {'id': id} + # interpret @ or @ suffix split = id.split('@') if len(split) == 2: @@ -171,7 +251,7 @@ def read(self, id): abort(400, _('Invalid revision format: %r') % e.args) elif len(split) > 2: abort(400, _('Invalid revision format: %r') % 'Too many "@" symbols') - + #check if package exists try: c.pkg_dict = get_action('package_show')(context, data_dict) @@ -181,7 +261,7 @@ def read(self, id): abort(404, _('Dataset not found')) except NotAuthorized: abort(401, _('Unauthorized to read package %s') % id) - + #set a cookie so we know whether to display the welcome message c.hide_welcome_message = bool(request.cookies.get('hide_welcome_message', False)) response.set_cookie('hide_welcome_message', '1', max_age=3600) #(make cross-site?) @@ -199,13 +279,14 @@ def read(self, id): if config.get('rdf_packages'): accept_header = request.headers.get('Accept', '*/*') for content_type, exts in negotiate(autoneg_cfg, accept_header): - if "html" not in exts: + if "html" not in exts: rdf_url = '%s%s.%s' % (config['rdf_packages'], c.pkg.id, exts[0]) redirect(rdf_url, code=303) break PackageSaver().render_package(c.pkg_dict, context) - return render('package/read.html') + + return render( self._read_template( package_type ) ) def comments(self, id): package_type = self._get_package_type(id) @@ -297,14 +378,14 @@ def history(self, id): ) feed.content_type = 'application/atom+xml' return feed.writeString('utf-8') - return render('package/history.html') + return render( self._history_template(c.pkg_dict['type'])) def new(self, data=None, errors=None, error_summary=None): - + package_type = request.path.strip('/').split('/')[0] if package_type == 'group': package_type = None - + context = {'model': model, 'session': model.Session, 'user': c.user or c.author, 'extras_as_string': True, 'save': 'save' in request.params,} @@ -321,7 +402,7 @@ def new(self, data=None, errors=None, error_summary=None): data = data or clean_dict(unflatten(tuplize_dict(parse_params( request.params, ignore_keys=[CACHE_PARAMETER])))) - c.pkg_json = json.dumps(data) + c.pkg_json = json.dumps(data) errors = errors or {} error_summary = error_summary or {} @@ -336,7 +417,7 @@ def new(self, data=None, errors=None, error_summary=None): c.form = render(self.package_form, extra_vars=vars) else: c.form = render(self._package_form(package_type=package_type), extra_vars=vars) - return render('package/new.html') + return render( self._new_template('')) def edit(self, id, data=None, errors=None, error_summary=None): @@ -438,22 +519,22 @@ def history_ajax(self, id): current_approved, approved = True, True else: current_approved = False - + data.append({'revision_id': revision['id'], 'message': revision['message'], 'timestamp': revision['timestamp'], 'author': revision['author'], 'approved': bool(revision['approved_timestamp']), 'current_approved': current_approved}) - + response.headers['Content-Type'] = 'application/json;charset=utf-8' return json.dumps(data) def _get_package_type(self, id): """ - Given the id of a package it determines the plugin to load + Given the id of a package it determines the plugin to load based on the package's type name (type). The plugin found - will be returned, or None if there is no plugin associated with + will be returned, or None if there is no plugin associated with the type. Uses a minimal context to do so. The main use of this method @@ -538,8 +619,8 @@ def _form_save_redirect(self, pkgname, action): url = url.replace('', pkgname) else: url = h.url_for(controller='package', action='read', id=pkgname) - redirect(url) - + redirect(url) + def _adjust_license_id_options(self, pkg, fs): options = fs.license_id.render_opts['options'] is_included = False @@ -574,7 +655,7 @@ def authz(self, id): def autocomplete(self): # DEPRECATED in favour of /api/2/util/dataset/autocomplete q = unicode(request.params.get('q', '')) - if not len(q): + if not len(q): return '' context = {'model': model, 'session': model.Session, diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py index 089c1b3fd31..18c9594cf94 100644 --- a/ckan/lib/plugins.py +++ b/ckan/lib/plugins.py @@ -127,7 +127,7 @@ def register_group_plugins(map): # Our version of routes doesn't allow the environ to be # passed into the match call and so we have to set it on the # map instead. This looks like a threading problem waiting - # to happen but it is executed sequentially from instead the + # to happen but it is executed sequentially from inside the # routing setup map.connect('%s_index' % group_type, '/%s' % group_type, @@ -168,6 +168,41 @@ class DefaultDatasetForm(object): 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 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' @@ -264,6 +299,34 @@ class DefaultGroupForm(object): 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 'group/new.html' + + def index_template(self): + """ + Returns a string representing the location of the template to be + rendered for the index page + """ + return 'group/index.html' + + def read_template(self): + """ + Returns a string representing the location of the template to be + rendered for the read page + """ + return 'group/read.html' + + def history_template(self): + """ + Returns a string representing the location of the template to be + rendered for the read page + """ + return 'group/history.html' + def group_form(self): return 'group/new_group_form.html' diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index e5dd055fade..a36547206af 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -174,15 +174,19 @@ def member_create(context, data_dict=None): """ model = context['model'] user = context['user'] + group = context['group'] + + rev = model.repo.new_revision() + rev.author = user + if 'message' in context: + rev.message = context['message'] + else: + rev.message = _(u'REST API: Create member object %s') % data_dict.get("name", "") - group_id = data_dict['group'] obj_id = data_dict['object'] obj_type = data_dict['object_type'] capacity = data_dict['capacity'] - if 'group' not in context: - context['group'] = group_id - # User must be able to update the group to add a member to it check_access('group_update', context, data_dict) @@ -190,15 +194,14 @@ def member_create(context, data_dict=None): member = model.Session.query(model.Member).\ 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").\ - filter(model.Member.capacity == capacity).first() + filter(model.Member.group_id == group.id).\ + 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, + group_id = group.id, capacity=capacity) model.Session.add(member) diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index b2d75458f47..a8368ec6195 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -82,21 +82,19 @@ def member_delete(context, data_dict=None): """ model = context['model'] user = context['user'] + group = context['group'] group_id = data_dict['group'] obj_id = data_dict['object'] obj_type = data_dict['object_type'] - if 'group' not in context: - context['group'] = group_id - # User must be able to update the group to remove a member from it check_access('group_update', context, data_dict) member = model.Session.query(model.Member).\ filter(model.Member.table_name == obj_type).\ filter(model.Member.table_id == obj_id).\ - filter(model.Member.group_id == group_id).\ + filter(model.Member.group_id == group.id).\ filter(model.Member.state == "active").first() if member: member.delete() diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index f5bae616a84..bf28588a769 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -117,18 +117,17 @@ def member_list(context, data_dict=None): """ model = context['model'] user = context['user'] + group = context['group'] group_id = data_dict['group'] obj_type = data_dict.get('object_type', None) capacity = data_dict.get('capacity', None) # User must be able to update the group to remove a member from it - if 'group' not in context: - context['group'] = group_id check_access('group_show', context, data_dict) q = model.Session.query(model.Member).\ - filter(model.Member.group_id == group_id).\ + filter(model.Member.group_id == group.id).\ filter(model.Member.state == "active") if obj_type: diff --git a/ckan/plugins/core.py b/ckan/plugins/core.py index 8cc05d844e4..5260164f2a0 100644 --- a/ckan/plugins/core.py +++ b/ckan/plugins/core.py @@ -63,14 +63,15 @@ def _get_service(plugin): if isinstance(plugin, basestring): try: + name = plugin (plugin,) = iter_entry_points( group=PLUGINS_ENTRY_POINT_GROUP, - name=plugin + name=name ) except ValueError: raise PluginNotFoundException(plugin) - return plugin.load()() + return plugin.load()(name=name) elif isinstance(plugin, _pca_Plugin): return plugin diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py index aa697d9aa33..a04d8fdf473 100644 --- a/ckan/plugins/interfaces.py +++ b/ckan/plugins/interfaces.py @@ -9,7 +9,7 @@ 'IMapper', 'ISession', 'IMiddleware', 'IAuthFunctions', - 'IDomainObjectModification', 'IGroupController', + 'IDomainObjectModification', 'IGroupController', 'IPackageController', 'IPluginObserver', 'IConfigurable', 'IConfigurer', 'IAuthorizer', 'IActions', 'IResourceUrlChange', 'IDatasetForm', @@ -78,7 +78,7 @@ def before_map(self, map): def after_map(self, map): """ - Called after routes map is set up. ``after_map`` can be used to add fall-back handlers. + Called after routes map is set up. ``after_map`` can be used to add fall-back handlers. :param map: Routes map object :returns: Modified version of the map object @@ -119,12 +119,12 @@ def after_insert(self, mapper, connection, instance): """ Receive an object instance after that instance is INSERTed. """ - + def after_update(self, mapper, connection, instance): """ Receive an object instance after that instance is UPDATEed. """ - + def after_delete(self, mapper, connection, instance): """ Receive an object instance after that instance is DELETEed. @@ -183,10 +183,10 @@ def notify(self, resource): class IGroupController(Interface): """ - Hook into the Group controller. These will + Hook into the Group controller. These will usually be called just before committing or returning the - respective object, i.e. all validation, synchronization - and authorization setup are complete. + respective object, i.e. all validation, synchronization + and authorization setup are complete. """ def read(self, entity): @@ -200,7 +200,7 @@ def edit(self, entity): def authz_add_role(self, object_role): pass - + def authz_remove_role(self, object_role): pass @@ -231,7 +231,7 @@ def edit(self, entity): def authz_add_role(self, object_role): pass - + def authz_remove_role(self, object_role): pass @@ -284,7 +284,7 @@ def before_view(self, pkg_dict): passed will be the one that gets sent to the template. ''' return pkg_dict - + class IPluginObserver(Interface): """ @@ -315,26 +315,26 @@ def after_unload(self, service): This method is passed the instantiated service object. """ -class IConfigurable(Interface): +class IConfigurable(Interface): """ Pass configuration to plugins and extensions """ - + def configure(self, config): """ Called by load_environment """ -class IConfigurer(Interface): +class IConfigurer(Interface): """ Configure CKAN (pylons) environment via the ``pylons.config`` object """ - + def update_config(self, config): """ Called by load_environment at earliest point when config is available to plugins. The config should be updated in place. - + :param config: ``pylons.config`` object """ @@ -364,14 +364,14 @@ def is_authorized(self, username, action, domain_obj): Should return True or False. A value of False will allow other Authorizers to run; True will shortcircuit and return. """ - + class IActions(Interface): """ Allow adding of actions to the logic layer. """ def get_actions(self): """ - Should return a dict, the keys being the name of the logic + Should return a dict, the keys being the name of the logic function and the values being the functions themselves. """ @@ -442,6 +442,37 @@ def package_types(self): ##### 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 + """ + + def comments_template(self): + """ + Returns a string representing the location of the template to be + rendered for the comments page + """ + + def search_template(self): + """ + Returns a string representing the location of the template to be + rendered for the search page (if present) + """ + + def read_template(self): + """ + Returns a string representing the location of the template to be + rendered for the read page + """ + + def history_template(self): + """ + Returns a string representing the location of the template to be + rendered for the history page + """ + + def package_form(self): """ Returns a string representing the location of the template to be @@ -532,7 +563,34 @@ def group_types(self): ##### 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. Uses the default_group_type configuration + option to determine which plugin to use the template from. + """ + + def index_template(self): + """ + Returns a string representing the location of the template to be + rendered for the index page. Uses the default_group_type configuration + option to determine which plugin to use the template from. + """ + + def read_template(self): + """ + Returns a string representing the location of the template to be + rendered for the read page + """ + + def history_template(self): + """ + Returns a string representing the location of the template to be + rendered for the history page + """ + + + def package_form(self): """ Returns a string representing the location of the template to be diff --git a/ckan/templates/package/read.rdf b/ckan/templates/package/read.rdf new file mode 100644 index 00000000000..172569eba83 --- /dev/null +++ b/ckan/templates/package/read.rdf @@ -0,0 +1,50 @@ + + + + + ${c.pkg_dict['name']} + + ${c.pkg_dict['name']} + ${c.pkg_dict['title']} + ${c.pkg_dict['notes']} + + + + + + + + + + ${ c.pkg_dict['author'] } + + + + + + ${ c.pkg_dict['maintainer'] } + + + + + ${ tag_dict["name"] } + + + + + + ${extra_dict.get('key','')} + ${extra_dict.get('value','')} + + + + + \ No newline at end of file diff --git a/ckan/tests/functional/test_package.py b/ckan/tests/functional/test_package.py index 6a1da9f71d2..218350b78cb 100644 --- a/ckan/tests/functional/test_package.py +++ b/ckan/tests/functional/test_package.py @@ -305,6 +305,14 @@ def test_read(self): assert 'book' in res, res assert 'This dataset satisfies the Open Definition' in res, res + def test_read_war_rdf(self): + name = u'warandpeace' + offset = url_for(controller='package', action='read', id=name + ".rdf") + res = self.app.get(offset) + ##TODO Ross To Fix + #assert 'A Wonderful Story' in res, res + + def test_read_war(self): name = u'warandpeace' c.hide_welcome_message = True @@ -372,7 +380,7 @@ def test_read_plugin_hook(self): plugins.unload(plugin) def test_resource_list(self): - # TODO restore this test. It doesn't make much sense with the + # TODO restore this test. It doesn't make much sense with the # present resource list design. name = 'annakarenina' cache_url = 'http://thedatahub.org/test_cache_url.csv' @@ -1031,7 +1039,7 @@ def test_edit_pkg_with_relationships(self): model.repo.new_revision() pkg.add_relationship(u'depends_on', anna) model.repo.commit_and_remove() - + # check relationship before the test rels = model.Package.by_name(self.editpkg_name).get_relationships() assert_equal(str(rels), '[<*PackageRelationship editpkgtest depends_on annakarenina>]') @@ -1046,7 +1054,7 @@ def test_edit_pkg_with_relationships(self): # check relationship still exists rels = model.Package.by_name(self.editpkg_name).get_relationships() assert_equal(str(rels), '[<*PackageRelationship editpkgtest depends_on annakarenina>]') - + finally: self._reset_data() @@ -1315,7 +1323,7 @@ def test_change_locale(self): assert 'Datensatz' in res.body, res.body finally: self.clear_language_setting() - + class TestSearch(TestPackageForm): pkg_names = [] diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index d6b15590af9..d1727552b6a 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -1296,7 +1296,7 @@ def test_28_group_package_show(self): assert group_names == set(['annakarenina', 'warandpeace']), group_names def test_29_group_package_show_pending(self): - context = {'model': model, 'session': model.Session, 'user': self.sysadmin_user.name} + context = {'model': model, 'session': model.Session, 'user': self.sysadmin_user.name, 'api_version': 2} group = { 'name': 'test_group_pending_package', 'packages': [{'id': model.Package.get('annakarenina').id}] @@ -1768,22 +1768,29 @@ def before_view(self, data_dict): return data_dict +MockPackageSearchPlugin().disable() + class TestSearchPluginInterface(WsgiAppCase): @classmethod def setup_class(cls): + MockPackageSearchPlugin().activate() + MockPackageSearchPlugin().enable() setup_test_search_index() CreateTestData.create() + MockPackageSearchPlugin().disable() @classmethod def teardown_class(cls): model.repo.rebuild_db() - def test_search_plugin_interface_search(self): - plugin = MockPackageSearchPlugin() - plugins.load(plugin) + def setup(self): + MockPackageSearchPlugin().enable() + def teardown(self): + MockPackageSearchPlugin().disable() + def test_search_plugin_interface_search(self): avoid = 'Tolstoy' search_params = '%s=1' % json.dumps({ 'q': '*:*', @@ -1797,11 +1804,8 @@ def test_search_plugin_interface_search(self): assert not avoid.lower() in result['title'].lower() assert results_dict['count'] == 1 - plugins.unload(plugin) def test_search_plugin_interface_abort(self): - plugin = MockPackageSearchPlugin() - plugins.load(plugin) search_params = '%s=1' % json.dumps({ 'q': '*:*', @@ -1814,11 +1818,9 @@ def test_search_plugin_interface_abort(self): res_dict = json.loads(res.body)['result'] assert res_dict['count'] == 0 assert len(res_dict['results']) == 0 - plugins.unload(plugin) def test_before_index(self): - plugin = MockPackageSearchPlugin() - plugins.load(plugin) + # no datasets get aaaaaaaa search_params = '%s=1' % json.dumps({ 'q': 'aaaaaaaa', @@ -1829,7 +1831,6 @@ def test_before_index(self): res_dict = json.loads(res.body)['result'] assert res_dict['count'] == 0 assert len(res_dict['results']) == 0 - plugins.unload(plugin) # all datasets should get abcabcabc search_params = '%s=1' % json.dumps({ @@ -1838,12 +1839,10 @@ def test_before_index(self): res = self.app.post('/api/action/package_search', params=search_params) res_dict = json.loads(res.body)['result'] - assert res_dict['count'] == 2 + assert res_dict['count'] == 2, res_dict['count'] assert len(res_dict['results']) == 2 def test_before_view(self): - plugin = MockPackageSearchPlugin() - plugins.load(plugin) res = self.app.get('/dataset/annakarenina') assert 'string_not_found_in_rest_of_template' in res.body @@ -1851,5 +1850,4 @@ def test_before_view(self): res = self.app.get('/dataset?q=') assert res.body.count('string_not_found_in_rest_of_template') == 2 - plugins.unload(plugin) diff --git a/ckan/tests/logic/test_member.py b/ckan/tests/logic/test_member.py index dcb6e8cba73..3c7aaa5d3a5 100644 --- a/ckan/tests/logic/test_member.py +++ b/ckan/tests/logic/test_member.py @@ -9,21 +9,26 @@ def setup_class(cls): cls.username = 'testsysadmin' cls.groupname = 'david' - model.Session.remove() - CreateTestData.create() - model.Session.remove() model.repo.new_revision() + CreateTestData.create() + cls.pkgs = [ + model.Package.by_name('warandpeace'), + model.Package.by_name('annakarenina'), + ] @classmethod def teardown_class(cls): model.repo.rebuild_db() def _build_context( self, obj, obj_type, capacity='member'): + grp = model.Group.by_name(self.groupname) ctx = { 'model': model, 'session': model.Session, - 'user':self.username} + 'user':self.username, + 'group': grp, + } dd = { - 'group': self.groupname, + 'group': grp, 'object': obj, 'object_type': obj_type, 'capacity': capacity } @@ -34,17 +39,15 @@ def _add_member( self, obj, obj_type, capacity): return get_action('member_create')(ctx,dd) def test_member_add(self): - res = self._add_member( 'warandpeace', 'package', 'member') + res = self._add_member( self.pkgs[0].id, 'package', 'member') assert 'capacity' in res and res['capacity'] == u'member' - assert 'group_id' in res and res['group_id'] == u'david' def test_member_list(self): - _ = self._add_member( 'warandpeace', 'package', 'member') - _ = self._add_member( 'annakarenina', 'package', 'member') + _ = self._add_member( self.pkgs[0].id, 'package', 'member') + _ = self._add_member( self.pkgs[1].id, 'package', 'member') ctx, dd = self._build_context('','package') res = get_action('member_list')(ctx,dd) - assert res[0][0] == 'warandpeace', res - assert res[1][0] == 'annakarenina', res + assert len(res) == 2, res ctx, dd = self._build_context('','user', 'admin') res = get_action('member_list')(ctx,dd) diff --git a/ckanext/publisher_form/forms.py b/ckanext/publisher_form/forms.py index 919127537d1..672209f6b4a 100644 --- a/ckanext/publisher_form/forms.py +++ b/ckanext/publisher_form/forms.py @@ -55,6 +55,36 @@ def update_config(self, config): # Override /group/* as the default groups urls config['ckan.default.group_type'] = 'publisher' + def new_template(self): + """ + Returns a string representing the location of the template to be + rendered for the new page + """ + return 'publisher_new.html' + + def index_template(self): + """ + Returns a string representing the location of the template to be + rendered for the index page + """ + return 'publisher_index.html' + + + def read_template(self): + """ + Returns a string representing the location of the template to be + rendered for the read page + """ + return 'publisher_read.html' + + def history_template(self): + """ + Returns a string representing the location of the template to be + rendered for the read page + """ + return 'publisher_history.html' + + def group_form(self): """ Returns a string representing the location of the template to be diff --git a/ckanext/publisher_form/templates/publisher_index.html b/ckanext/publisher_form/templates/publisher_index.html new file mode 100644 index 00000000000..321e45197b2 --- /dev/null +++ b/ckanext/publisher_form/templates/publisher_index.html @@ -0,0 +1,24 @@ + + + Publishers of Datasets + Publishers of Datasets + + +
  • +

    What Are Publishers?

    + Whilst tags are great at collecting datasets together, there are occasions when you want to restrict users from editing a collection. A publisher can be set-up to specify which users have permission to add or remove datasets from it. +
  • +
    + + +
    + ${c.page.pager()} + ${group_list_from_dict(c.page.items)} + ${c.page.pager()} +
    + + + diff --git a/ckanext/publisher_form/templates/publisher_layout.html b/ckanext/publisher_form/templates/publisher_layout.html new file mode 100644 index 00000000000..25052d90311 --- /dev/null +++ b/ckanext/publisher_form/templates/publisher_layout.html @@ -0,0 +1,50 @@ + + + +
      +
    • ${h.subnav_named_route(c, h.icon('group') + _('View'), 'publisher_read',controller='group', action='read', id=c.group.name)}
    • +
    • ${h.subnav_named_route(c, h.icon('page_white_stack') + _('History'), 'publisher_action', controller='group', action='history', id=c.group.name)}
    • +   |   + +
    • + + ${h.subnav_named_route( c,h.icon('group_edit') + _('Edit'), 'publisher_action', action='edit', id=c.group.name )} +
    • +
    • + ${h.subnav_named_route(c, h.icon('lock') + _('Authorization'), 'publisher_action', controller='group', action='authz', id=c.group.name)} +
    • + + +
    +
      +
    • + ${h.subnav_named_route(c, h.icon('group') + _('List Publishers'), "publisher_index", action="index" )} +
    • +
    • + ${h.subnav_link(c, h.icon('group_add') + _('Login to Add a Publisher'), controller='group', action='new')} +
    • +
    +
    + + + diff --git a/ckanext/publisher_form/templates/publisher_new.html b/ckanext/publisher_form/templates/publisher_new.html new file mode 100644 index 00000000000..33c3ad863dd --- /dev/null +++ b/ckanext/publisher_form/templates/publisher_new.html @@ -0,0 +1,14 @@ + + + Add A Publisher + Add A Publisher + +
    + ${Markup(c.form)} +
    + + + + diff --git a/ckanext/publisher_form/templates/publisher_read.html b/ckanext/publisher_form/templates/publisher_read.html new file mode 100644 index 00000000000..4002f264694 --- /dev/null +++ b/ckanext/publisher_form/templates/publisher_read.html @@ -0,0 +1,100 @@ + + + + ${c.group.display_name} + ${c.group.display_name} + + + + +
  • +
      + +
    • +

      Administrators

      +
        +
      • ${h.linked_user(admin)}
      • +
      +
    • +
      +
    +
  • + ${facet_sidebar('tags')} + ${facet_sidebar('res_format')} +
    + + +

    State: ${c.group['state']}

    +
    +
    + ${c.description_formatted} +
    +
    + +
    +
    +

    Datasets

    + + + ${field_list()} + +

    You searched for "${c.q}". ${c.page.item_count} datasets found.

    + ${c.page.pager()} + + +
    + + ${package.get('title') or package.get('name')} +    + + + + ${resource.get('format')} + + + +

    ${h.markdown_extract(package.notes)}

    + + + + + [Open Data] + + + + ${h.icon('lock')} Not Openly Licensed + + +
    +
    + ${c.page.pager()} +
    +
    + + + + + diff --git a/ckanext/test_tag_vocab_plugin.py b/ckanext/test_tag_vocab_plugin.py index 65eea1c566a..8d88650b7bb 100644 --- a/ckanext/test_tag_vocab_plugin.py +++ b/ckanext/test_tag_vocab_plugin.py @@ -25,6 +25,21 @@ def is_fallback(self): def package_types(self): return ["mock_vocab_tags_plugin"] + def new_template(self): + return 'package/new.html' + + def comments_template(self): + return 'package/comments.html' + + def search_template(self): + return 'package/search.html' + + def read_template(self): + return 'package/read.html' + + def history_template(self): + return 'package/history.html' + def package_form(self): return 'package/new_package_form.html' diff --git a/doc/install-from-source.rst b/doc/install-from-source.rst index 5e9f621ca24..403ff331574 100644 --- a/doc/install-from-source.rst +++ b/doc/install-from-source.rst @@ -219,7 +219,6 @@ Set appropriate values for the ``ckan.site_id`` and ``solr_url`` config variable :: -<<<<<<< HEAD ckan.site_id=my_ckan_instance solr_url=http://127.0.0.1:8983/solr diff --git a/pip-requirements.txt b/pip-requirements.txt index 9ff9d104906..4460f149f3e 100644 --- a/pip-requirements.txt +++ b/pip-requirements.txt @@ -5,11 +5,11 @@ # # pip install --ignore-installed -r pip-requirements.txt --e git+https://github.com/okfn/ckan@master#egg=ckan +-e git+https://github.com/okfn/ckan@release-v1.6.1#egg=ckan # CKAN dependencies --r https://github.com/okfn/ckan/raw/master/requires/lucid_conflict.txt --r https://github.com/okfn/ckan/raw/master/requires/lucid_present.txt --r https://github.com/okfn/ckan/raw/master/requires/lucid_missing.txt +-r https://github.com/okfn/ckan/raw/release-v1.6.1/requires/lucid_conflict.txt +-r https://github.com/okfn/ckan/raw/release-v1.6.1/requires/lucid_present.txt +-r https://github.com/okfn/ckan/raw/release-v1.6.1/requires/lucid_missing.txt # NOTE: Developers, please do not edit this file. Changes should go in the # appropriate files in the `requires' directory.