diff --git a/ckan/config/environment.py b/ckan/config/environment.py index c6e13b62af7..1b0151e11b1 100644 --- a/ckan/config/environment.py +++ b/ckan/config/environment.py @@ -173,6 +173,7 @@ def find_controller(self, controller): search.check_solr_schema_version() config['routes.map'] = routing.make_map() + config['routes.named_routes'] = routing.named_routes config['pylons.app_globals'] = app_globals.app_globals # initialise the globals config['pylons.app_globals']._init() diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 7187f4f4560..d3444701c1f 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -5,13 +5,65 @@ refer to the routes manual at http://routes.groovie.org/docs/ """ +import re + from pylons import config -from routes import Mapper +from routes import Mapper as _Mapper + from ckan.plugins import PluginImplementations, IRoutes +named_routes = {} routing_plugins = PluginImplementations(IRoutes) + +class Mapper(_Mapper): + ''' This Mapper allows us to intercept the connect calls used by routes + so that we can collect named routes and later use them to create links + via some helper functions like build_nav(). ''' + + def connect(self, *args, **kw): + '''Connect a new route, storing any named routes for later. + + This custom connect() method wraps the standard connect() method, + and additionally saves any named routes that are connected in a dict + ckan.routing.named_routes, which ends up being accessible via the + Pylons config as config['routes.named_routes']. + + Also takes some additional params: + + :param ckan_icon: name of the icon to be associated with this route, + e.g. 'group', 'time' + :type ckan_icon: string + :param highlight_actions: space-separated list of controller actions + that should be treated as the same as this named route for menu + highlighting purposes, e.g. 'index search' + :type highlight_actions: string + + ''' + ckan_icon = kw.pop('ckan_icon', None) + highlight_actions = kw.pop('highlight_actions', kw.get('action', '')) + out = _Mapper.connect(self, *args, **kw) + if len(args) == 1 or args[0].startswith('_redirect_'): + return out + # we have a named route + needed = [] + matches = re.findall('\{([^:}]*)(\}|:)', args[1]) + for match in matches: + needed.append(match[0]) + route_data = { + 'icon': ckan_icon, + # needed lists the names of the parameters that need defining + # for the route to be generated + 'needed': needed, + 'controller': kw.get('controller'), + 'action': kw.get('action', ''), + 'highlight_actions': highlight_actions + } + named_routes[args[0]] = route_data + return out + + def make_map(): """Create, configure and return the routes Mapper""" # import controllers here rather than at root level because @@ -158,16 +210,18 @@ def make_map(): action='edit') m.connect('related_delete', '/dataset/{id}/related/delete/{related_id}', action='delete') - m.connect('related_list', '/dataset/{id}/related', action='list') + m.connect('related_list', '/dataset/{id}/related', action='list', + ckan_icon='picture') m.connect('related_read', '/apps/{id}', action='read') m.connect('related_dashboard', '/apps', action='dashboard') with SubMapper(map, controller='package') as m: - m.connect('/dataset', action='search') + m.connect('search', '/dataset', action='search', + highlight_actions='index search') + m.connect('add dataset', '/dataset/new', action='new') m.connect('/dataset/{action}', requirements=dict(action='|'.join([ 'list', - 'new', 'autocomplete', 'search' ])) @@ -188,8 +242,6 @@ def make_map(): 'history', 'read_ajax', 'history_ajax', - 'activity', - 'followers', 'follow', 'activity', 'unfollow', @@ -197,9 +249,14 @@ def make_map(): 'api_data', ])) ) + m.connect('dataset_followers', '/dataset/followers/{id}', + action='followers', ckan_icon='group') + m.connect('dataset_activity', '/dataset/activity/{id}', + action='activity', ckan_icon='time') m.connect('/dataset/activity/{id}/{offset}', action='activity') m.connect('/dataset/{id}.{format}', action='read') - m.connect('/dataset/{id}', action='read') + m.connect('dataset_read', '/dataset/{id}', action='read', + ckan_icon='sitemap') m.connect('/dataset/{id}/resource/{resource_id}', action='resource_read') m.connect('/dataset/{id}/resource_delete/{resource_id}', @@ -226,7 +283,8 @@ def make_map(): # These named routes are used for custom group forms which will use the # names below based on the group.type ('group' is the default type) with SubMapper(map, controller='group') as m: - m.connect('group_index', '/group', action='index') + m.connect('group_index', '/group', action='index', + highlight_actions='index search') m.connect('group_list', '/group/list', action='list') m.connect('group_new', '/group/new', action='new') m.connect('group_action', '/group/{action}/{id}', @@ -250,7 +308,7 @@ def make_map(): # organizations these basically end up being the same as groups with SubMapper(map, controller='organization') as m: - m.connect('/organization', action='index') + m.connect('organizations_index', '/organization', action='index') m.connect('/organization/list', action='list') m.connect('/organization/new', action='new') m.connect('/organization/{action}/{id}', @@ -283,16 +341,18 @@ def make_map(): # Note: openid users have slashes in their ids, so need the wildcard # in the route. m.connect('/user/activity/{id}/{offset}', action='activity') - m.connect('/user/activity/{id}', action='activity') + m.connect('user_activity_stream', '/user/activity/{id}', + action='activity', ckan_icon='time') m.connect('/dashboard/{offset}', action='dashboard') m.connect('/dashboard', action='dashboard') - m.connect('/user/follow/{id}', action='follow') + m.connect('user_follow', '/user/follow/{id}', action='follow') m.connect('/user/unfollow/{id}', action='unfollow') - m.connect('/user/followers/{id:.*}', action='followers') + m.connect('user_followers', '/user/followers/{id:.*}', + action='followers', ckan_icon='group') m.connect('/user/edit/{id:.*}', action='edit') m.connect('/user/reset/{id:.*}', action='perform_reset') - m.connect('/user/register', action='register') - m.connect('/user/login', action='login') + m.connect('register', '/user/register', action='register') + m.connect('login', '/user/login', action='login') m.connect('/user/_logout', action='logout') m.connect('/user/logged_in', action='logged_in') m.connect('/user/logged_out', action='logged_out') @@ -300,8 +360,9 @@ def make_map(): m.connect('/user/reset', action='request_reset') m.connect('/user/me', action='me') m.connect('/user/set_lang/{lang}', action='set_lang') - m.connect('/user/{id:.*}', action='read') - m.connect('/user', action='index') + m.connect('user_datasets', '/user/{id:.*}', action='read', + ckan_icon='sitemap') + m.connect('user_index', '/user', action='index') with SubMapper(map, controller='revision') as m: m.connect('/revision', action='index') diff --git a/ckan/controllers/related.py b/ckan/controllers/related.py index d624801cc5e..4b1baefb4b8 100644 --- a/ckan/controllers/related.py +++ b/ckan/controllers/related.py @@ -122,7 +122,6 @@ def list(self, id): except logic.NotAuthorized: base.abort(401, base._('Unauthorized to read package %s') % id) - c.action = 'related' return base.render("package/related_list.html") def _edit_or_new(self, id, related_id, is_edit): diff --git a/ckan/i18n/check_po_files.py b/ckan/i18n/check_po_files.py index a918087d232..10997f3fa3e 100755 --- a/ckan/i18n/check_po_files.py +++ b/ckan/i18n/check_po_files.py @@ -11,7 +11,6 @@ ''' import re -import polib import paste.script.command def simple_conv_specs(s): @@ -98,6 +97,8 @@ class CheckPoFiles(paste.script.command.Command): parser = paste.script.command.Command.standard_parser(verbose=True) def command(self): + import polib + test_simple_conv_specs() test_mapping_keys() test_replacement_fields() diff --git a/ckan/lib/app_globals.py b/ckan/lib/app_globals.py index 387371a2f6c..911d32bd9de 100644 --- a/ckan/lib/app_globals.py +++ b/ckan/lib/app_globals.py @@ -29,6 +29,38 @@ 'ckan.site_custom_css', ] +config_details = { + 'ckan.favicon': {}, # default gets set in config.environment.py + 'ckan.template_head_end': {}, + 'ckan.template_footer_end': {}, + # has been setup in load_environment(): + 'ckan.site_id': {}, + 'ckan.recaptcha.publickey': {'name': 'recaptcha_publickey'}, + 'ckan.recaptcha.privatekey': {'name': 'recaptcha_publickey'}, + 'ckan.template_title_deliminater': {'default': '-'}, + 'ckan.template_head_end': {}, + 'ckan.template_footer_end': {}, + 'ckan.dumps_url': {}, + 'ckan.dumps_format': {}, + 'ckan.api_url': {}, + + # split string + 'search.facets': {'default': 'groups tags res_format license', + 'type': 'split', + 'name': 'facets'}, + 'package_hide_extras': {'type': 'split'}, + 'plugins': {'type': 'split'}, + + # bool + 'openid_enabled': {'default': 'true', 'type' : 'bool'}, + 'debug': {'default': 'false', 'type' : 'bool'}, + 'ckan.debug_supress_header' : {'default': 'false', 'type' : 'bool'}, + + # int + 'ckan.datasets_per_page': {'default': '20', 'type': 'int'}, +} + + # A place to store the origional config options of we override them _CONFIG_CACHE = {} @@ -143,25 +175,26 @@ def _init(self): facets = config.get('search.facets', 'groups tags res_format license capacity') self.facets = facets.split() - # has been setup in load_environment(): - self.site_id = config.get('ckan.site_id') - - self.template_head_end = config.get('ckan.template_head_end', '') - self.template_footer_end = config.get('ckan.template_footer_end', '') - - # hide these extras fields on package read - package_hide_extras = config.get('package_hide_extras', '').split() - self.package_hide_extras = package_hide_extras - - self.openid_enabled = asbool(config.get('openid_enabled', 'true')) + # process the config_details to set globals + for name, options in config_details.items(): + if 'name' in options: + key = options['name'] + elif name.startswith('ckan.'): + key = name[5:] + else: + key = name + value = config.get(name, options.get('default', '')) - self.recaptcha_publickey = config.get('ckan.recaptcha.publickey', '') - self.recaptcha_privatekey = config.get('ckan.recaptcha.privatekey', '') + data_type = options.get('type') + if data_type == 'bool': + value = asbool(value) + elif data_type == 'int': + value = int(value) + elif data_type == 'split': + value = value.split() - datasets_per_page = int(config.get('ckan.datasets_per_page', '20')) - self.datasets_per_page = datasets_per_page + setattr(self, key, value) - self.debug_supress_header = asbool(config.get('ckan.debug_supress_header', 'false')) app_globals = _Globals() del _Globals diff --git a/ckan/lib/base.py b/ckan/lib/base.py index 03c35c2ef1c..fa072e9cdb4 100644 --- a/ckan/lib/base.py +++ b/ckan/lib/base.py @@ -128,6 +128,10 @@ def render_template(): # Jinja2 templates if template_type == 'jinja2': + # We don't want to have the config in templates it should be + # accessed via g (app_globals) as this gives us flexability such + # as changing via database settings. + del globs['config'] # TODO should we raise error if genshi filters?? return render_jinja2(template_name, globs) diff --git a/ckan/lib/cli.py b/ckan/lib/cli.py index 5cc24fc04b1..fe2b446669e 100644 --- a/ckan/lib/cli.py +++ b/ckan/lib/cli.py @@ -1,3 +1,5 @@ +import collections +import csv import os import datetime import sys @@ -579,9 +581,12 @@ class UserCmd(CkanCommand): user - lists users user list - lists users user - shows user properties - user add [apikey=] [password=] - - add a user (prompts for password if - not supplied) + user add [=] + - add a user (prompts for password + if not supplied). + Field can be: apikey + password + email user setpass - set user password (prompts) user remove - removes user from users user search - searches for a user name @@ -678,53 +683,37 @@ def add(self): if len(self.args) < 2: print 'Need name of the user.' - return - username = self.args[1] - user = model.User.by_name(unicode(username)) - if user: - print 'User "%s" already found' % username sys.exit(1) + username = self.args[1] - # parse args - apikey = None - password = None - args = self.args[2:] - if len(args) == 1 and not (args[0].startswith('password') or \ - args[0].startswith('apikey')): - # continue to support the old syntax of just supplying - # the apikey - apikey = args[0] - else: - # new syntax: password=foo apikey=bar - for arg in args: - split = arg.find('=') - if split == -1: - split = arg.find(' ') - if split == -1: - raise ValueError('Could not parse arg: %r (expected "--