diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d654a4757f9..5bfbc1701a7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,15 @@ Changelog --------- +v2.2 +==== + +API changes and deprecations: + +* The edit() and after_update() methods of IPackageController plugins are now + called when updating a resource using the web frontend or the + resource_update API action [#1052] + v2.0.1 2013-06-11 ================= @@ -423,7 +432,7 @@ v1.5 2011-11-07 Major: * New visual theme (#1108) * Package & Resource edit overhaul (#1294/#1348/#1351/#1368/#1296) - * JS and CSS reorganisation (#1282, #1349, #1380) + * JS and CSS reorganization (#1282, #1349, #1380) * Apache Solr used for search in core instead of Postgres (#1275, #1361, #1365) * Authorization system now embedded in the logic layer (#1253) * Captcha added for user registration (#1307, #1431) diff --git a/bin/ckan_edit_local.py b/bin/ckan_edit_local.py index 680700e9ba5..6e13575eb25 100644 --- a/bin/ckan_edit_local.py +++ b/bin/ckan_edit_local.py @@ -84,6 +84,7 @@ def canada_extras(): 'Level of Government':'level_of_government', } license_mapping = { + # CS: bad_spelling ignore 'http://geogratis.ca/geogratis/en/licence.jsp':'geogratis', 'Crown Copyright':'canada-crown', } diff --git a/ckan/__init__.py b/ckan/__init__.py index bcc319eb7d8..c008dc65ba7 100644 --- a/ckan/__init__.py +++ b/ckan/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.1a' +__version__ = '2.2a' __description__ = 'Comprehensive Knowledge Archive Network (CKAN) Software' __long_description__ = \ diff --git a/ckan/config/deployment.ini_tmpl b/ckan/config/deployment.ini_tmpl index 53ddc80c108..738ff22858d 100644 --- a/ckan/config/deployment.ini_tmpl +++ b/ckan/config/deployment.ini_tmpl @@ -107,7 +107,7 @@ ckan.preview.loadable = html htm rdf+xml owl+xml xml n3 n-triples turtle plain a ckan.locale_default = en ckan.locale_order = en pt_BR ja it cs_CZ ca es fr el sv sr sr@latin no sk fi ru de pl nl bg ko_KR hu sa sl lv ckan.locales_offered = -ckan.locales_filtered_out = +ckan.locales_filtered_out = en_GB ## Feeds Settings diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 0d3c00c5838..6423cad79cf 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -279,7 +279,6 @@ def make_map(): requirements=dict(action='|'.join([ 'edit', 'delete', - 'members', 'member_new', 'member_delete', 'history', @@ -291,6 +290,10 @@ def make_map(): ]))) m.connect('group_about', '/group/about/{id}', action='about', ckan_icon='info-sign'), + m.connect('group_edit', '/group/edit/{id}', action='edit', + ckan_icon='edit') + m.connect('group_members', '/group/members/{id}', action='members', + ckan_icon='group'), m.connect('group_activity', '/group/activity/{id}/{offset}', action='activity', ckan_icon='time'), m.connect('group_read', '/group/{id}', action='read', diff --git a/ckan/controllers/api.py b/ckan/controllers/api.py index 0eebd90f2e3..22ea7d4f5c3 100644 --- a/ckan/controllers/api.py +++ b/ckan/controllers/api.py @@ -158,7 +158,7 @@ def action(self, logic_function, ver=None): except KeyError: log.error('Can\'t find logic function: %s' % logic_function) return self._finish_bad_request( - _('Action name not known: %s') % str(logic_function)) + _('Action name not known: %s') % logic_function) context = {'model': model, 'session': model.Session, 'user': c.user, 'api_version': ver} @@ -169,9 +169,9 @@ def action(self, logic_function, ver=None): request_data = self._get_request_data(try_url_params= side_effect_free) except ValueError, inst: - log.error('Bad request data: %s' % str(inst)) + log.error('Bad request data: %s' % inst) return self._finish_bad_request( - _('JSON Error: %s') % str(inst)) + _('JSON Error: %s') % inst) if not isinstance(request_data, dict): # this occurs if request_data is blank log.error('Bad request data - not dict: %r' % request_data) @@ -210,6 +210,7 @@ def action(self, logic_function, ver=None): error_dict['__type'] = 'Validation Error' return_dict['error'] = error_dict return_dict['success'] = False + # CS nasty_string ignore log.error('Validation error: %r' % str(e.error_dict)) return self._finish(409, return_dict, content_type='json') except search.SearchQueryError, e: @@ -334,7 +335,7 @@ def create(self, ver=None, register=None, subregister=None, data_dict.update(request_data) except ValueError, inst: return self._finish_bad_request( - _('JSON Error: %s') % str(inst)) + _('JSON Error: %s') % inst) action = self._get_action_from_map(action_map, register, subregister) if not action: @@ -357,6 +358,7 @@ def create(self, ver=None, register=None, subregister=None, extra_msg = e.extra_msg return self._finish_not_found(extra_msg) except ValidationError, e: + # CS: nasty_string ignore log.error('Validation error: %r' % str(e.error_dict)) return self._finish(409, e.error_dict, content_type='json') except DataError, e: @@ -396,7 +398,7 @@ def update(self, ver=None, register=None, subregister=None, data_dict.update(request_data) except ValueError, inst: return self._finish_bad_request( - _('JSON Error: %s') % str(inst)) + _('JSON Error: %s') % inst) action = self._get_action_from_map(action_map, register, subregister) if not action: @@ -412,6 +414,7 @@ def update(self, ver=None, register=None, subregister=None, extra_msg = e.extra_msg return self._finish_not_found(extra_msg) except ValidationError, e: + # CS: nasty_string ignore log.error('Validation error: %r' % str(e.error_dict)) return self._finish(409, e.error_dict, content_type='json') except DataError, e: @@ -459,6 +462,7 @@ def delete(self, ver=None, register=None, subregister=None, extra_msg = e.extra_msg return self._finish_not_found(extra_msg) except ValidationError, e: + # CS: nasty_string ignore log.error('Validation error: %r' % str(e.error_dict)) return self._finish(409, e.error_dict, content_type='json') diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 529d108ecdb..ccef46bfe78 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -281,7 +281,7 @@ def pager_url(q=None, page=None): default_facet_titles = {'groups': _('Groups'), 'tags': _('Tags'), 'res_format': _('Formats'), - 'license': _('License')} + 'license_id': _('License')} for facet in g.facets: if facet in default_facet_titles: @@ -524,7 +524,7 @@ def _save_edit(self, id, context): if id != group['name']: self._force_reindex(group) - h.redirect_to('%s_read' % str(group['type']), id=group['name']) + h.redirect_to('%s_read' % group['type'], id=group['name']) except NotAuthorized: abort(401, _('Unauthorized to read group %s') % id) except NotFound, e: diff --git a/ckan/controllers/organization.py b/ckan/controllers/organization.py index b411f8e2b31..b44d95e057f 100644 --- a/ckan/controllers/organization.py +++ b/ckan/controllers/organization.py @@ -19,7 +19,7 @@ def _group_form(self, group_type=None): return 'organization/new_organization_form.html' def _form_to_db_schema(self, group_type=None): - return lookup_group_plugin(group_type).form_to_db_schema() + return group.lookup_group_plugin(group_type).form_to_db_schema() def _db_to_form_schema(self, group_type=None): '''This is an interface to manipulate data from the database @@ -48,7 +48,7 @@ def _read_template(self, group_type): return 'organization/read.html' def _history_template(self, group_type): - return lookup_group_plugin(group_type).history_template() + return group.lookup_group_plugin(group_type).history_template() def _edit_template(self, group_type): return 'organization/edit.html' diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index 65541ee33bf..834b3ba38b0 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -344,6 +344,11 @@ def read(self, id, format='html'): c.current_package_id = c.pkg.id c.related_count = c.pkg.related_count + # can the resources be previewed? + for resource in c.pkg_dict['resources']: + resource['can_be_previewed'] = self._resource_preview( + {'resource': resource, 'package': c.pkg_dict}) + self._setup_template_variables(context, {'id': id}, package_type=package_type) @@ -1142,8 +1147,17 @@ def resource_read(self, id, resource_id): c.datastore_api = '%s/api/action' % config.get('ckan.site_url', '').rstrip('/') c.related_count = c.pkg.related_count + + c.resource['can_be_previewed'] = self._resource_preview( + {'resource': c.resource, 'package': c.package}) return render('package/resource_read.html') + def _resource_preview(self, data_dict): + return bool(datapreview.res_format(data_dict['resource']) + in datapreview.direct() + datapreview.loadable() + or datapreview.get_preview_plugin( + data_dict, return_first=True)) + def resource_download(self, id, resource_id): """ Provides a direct download by redirecting the user to the url stored @@ -1319,9 +1333,9 @@ def resource_datapreview(self, id, resource_id): ''' Embeded page for a resource data-preview. - Depending on the type, different previews are loaded. - This could be an img tag where the image is loaded directly or an iframe that - embeds a webpage, recline or a pdf preview. + Depending on the type, different previews are loaded. This could be an + img tag where the image is loaded directly or an iframe that embeds a + webpage, recline or a pdf preview. ''' context = { 'model': model, @@ -1335,30 +1349,17 @@ def resource_datapreview(self, id, resource_id): c.package = get_action('package_show')(context, {'id': id}) data_dict = {'resource': c.resource, 'package': c.package} - on_same_domain = datapreview.resource_is_on_same_domain(data_dict) - data_dict['resource']['on_same_domain'] = on_same_domain - - # FIXME this wants to not use plugins as it is an imported name - # and we already import it an p should really only be in - # extensu=ions in my opinion also just make it look nice and be - # readable grrrrrr - plugins = p.PluginImplementations(p.IResourcePreview) - plugins_that_can_preview = [plugin for plugin in plugins - if plugin.can_preview(data_dict)] - if len(plugins_that_can_preview) == 0: - abort(409, _('No preview has been defined.')) - if len(plugins_that_can_preview) > 1: - log.warn('Multiple previews are possible. {0}'.format( - plugins_that_can_preview)) - plugin = plugins_that_can_preview[0] - plugin.setup_template_variables(context, data_dict) + preview_plugin = datapreview.get_preview_plugin(data_dict) - c.resource_json = json.dumps(c.resource) + if preview_plugin is None: + abort(409, _('No preview has been defined.')) + preview_plugin.setup_template_variables(context, data_dict) + c.resource_json = json.dumps(c.resource) except NotFound: abort(404, _('Resource not found')) except NotAuthorized: abort(401, _('Unauthorized to read resource %s') % id) else: - return render(plugin.preview_template(context, data_dict)) + return render(preview_plugin.preview_template(context, data_dict)) diff --git a/ckan/lib/activity_streams.py b/ckan/lib/activity_streams.py index d558994d5f6..47467cf2ede 100644 --- a/ckan/lib/activity_streams.py +++ b/ckan/lib/activity_streams.py @@ -256,7 +256,7 @@ def activity_list_to_html(context, activity_stream, extra_vars): if not activity_type in activity_stream_string_functions: raise NotImplementedError("No activity renderer for activity " - "type '%s'" % str(activity_type)) + "type '%s'" % activity_type) if activity_type in activity_stream_string_icons: activity_icon = activity_stream_string_icons[activity_type] diff --git a/ckan/lib/base.py b/ckan/lib/base.py index 7227734ddf4..5c755650c21 100644 --- a/ckan/lib/base.py +++ b/ckan/lib/base.py @@ -263,6 +263,12 @@ def _identify_user(self): if not c.user: self._identify_user_default() + # If we have a user but not the userobj let's get the userobj. This + # means that IAuthenticator extensions do not need to access the user + # model directly. + if c.user and not c.userobj: + c.userobj = model.User.by_name(c.user) + # general settings if c.user: c.author = c.user diff --git a/ckan/lib/celery_app.py b/ckan/lib/celery_app.py index 95f15937f18..f95fcae3447 100644 --- a/ckan/lib/celery_app.py +++ b/ckan/lib/celery_app.py @@ -4,7 +4,6 @@ from pylons import config as pylons_config from pkg_resources import iter_entry_points, VersionConflict -#from celery.loaders.base import BaseLoader log = logging.getLogger(__name__) @@ -49,16 +48,16 @@ log.critical(error) pass -celery.conf.update(default_config) -celery.loader.conf.update(default_config) - try: for key, value in config.items('app:celery'): if key in LIST_PARAMS: - celery.conf[key.upper()] = value.split() - celery.loader.conf[key.upper()] = value.split() + default_config[key.upper()] = value.split() else: - celery.conf[key.upper()] = value - celery.loader.conf[key.upper()] = value.split() + default_config[key.upper()] = value except ConfigParser.NoSectionError: pass + +# Thes update of configuration means it is only possible to set each +# key once so this is done once all of the options have been decided. +celery.conf.update(default_config) +celery.loader.conf.update(default_config) diff --git a/ckan/lib/datapreview.py b/ckan/lib/datapreview.py index 27e7804bc83..1600aad81a3 100644 --- a/ckan/lib/datapreview.py +++ b/ckan/lib/datapreview.py @@ -6,6 +6,7 @@ """ import urlparse +import logging import pylons.config as config @@ -16,6 +17,27 @@ 'n3', 'n-triples', 'turtle', 'plain', 'atom', 'rss', 'txt'] +log = logging.getLogger(__name__) + + +def direct(): + ''' Directly embedable formats.''' + direct_embed = config.get('ckan.preview.direct', '').split() + return direct_embed or DEFAULT_DIRECT_EMBED + + +def loadable(): + ''' Iframe loadable formats. ''' + loadable_in_iframe = config.get('ckan.preview.loadable', '').split() + return loadable_in_iframe or DEFAULT_LOADABLE_IFRAME + + +def res_format(resource): + ''' The assummed resource format in lower case. ''' + if not resource['url']: + return None + return (resource['format'] or resource['url'].split('.')[-1]).lower() + def compare_domains(urls): ''' Return True if the domains of the provided are the same. @@ -41,7 +63,7 @@ def compare_domains(urls): return True -def resource_is_on_same_domain(data_dict): +def _on_same_domain(data_dict): # compare CKAN domain and resource URL ckan_url = config.get('ckan.site_url', '//localhost:5000') resource_url = data_dict['resource']['url'] @@ -49,14 +71,53 @@ def resource_is_on_same_domain(data_dict): return compare_domains([ckan_url, resource_url]) -def can_be_previewed(data_dict): - ''' - Determines whether there is an extension that can preview the resource. +def get_preview_plugin(data_dict, return_first=False): + '''Determines whether there is an extension that can preview the resource. :param data_dict: contains a resource and package dict. The resource dict has to have a value for ``on_same_domain`` :type data_dict: dictionary - ''' - data_dict['resource']['on_same_domain'] = resource_is_on_same_domain(data_dict) - plugins = p.PluginImplementations(p.IResourcePreview) - return any(plugin.can_preview(data_dict) for plugin in plugins) + + :param return_first: If True return the first plugin that can preview + :type return_first: bool + + Returns a dict of plugins that can preview or ones that are fixable''' + + data_dict['resource']['on_same_domain'] = _on_same_domain(data_dict) + + plugins_that_can_preview = [] + plugins_fixable = [] + for plugin in p.PluginImplementations(p.IResourcePreview): + p_info = {'plugin': plugin, 'quality': 1} + data = plugin.can_preview(data_dict) + # old school plugins return true/False + if isinstance(data, bool): + p_info['can_preview'] = data + else: + # new school provide a dict + p_info.update(data) + # if we can preview + if p_info['can_preview']: + if return_first: + plugin + plugins_that_can_preview.append(p_info) + elif p_info.get('fixable'): + plugins_fixable.append(p_info) + + num_plugins = len(plugins_that_can_preview) + if num_plugins == 0: + # we didn't find any. see if any could be made to work + for plug in plugins_fixable: + log.info('%s would allow previews to fix: %s' % ( + plug['plugin'], plug['fixable'])) + preview_plugin = None + elif num_plugins == 1: + # just one available + preview_plugin = plugins_that_can_preview[0]['plugin'] + else: + # multiple plugins so get the best one + plugs = [pl['plugin'] for pl in plugins_that_can_preview] + log.warn('Multiple previews are possible. {0}'.format(plugs)) + preview_plugin = max(plugins_that_can_preview, + key=lambda x: x['quality'])['plugin'] + return preview_plugin diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 891660f3b7e..5c8a12d5613 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -611,6 +611,13 @@ def check_access(action, data_dict=None): return authorized +def get_action(action_name, data_dict=None): + '''Calls an action function from a template.''' + if data_dict is None: + data_dict = {} + return logic.get_action(action_name)({}, data_dict) + + def linked_user(user, maxlength=0, avatar=20): if user in [model.PSEUDO_USER__LOGGED_IN, model.PSEUDO_USER__VISITOR]: return user @@ -1457,7 +1464,12 @@ def format_resource_items(items): continue # size is treated specially as we want to show in MiB etc if key == 'size': - value = formatters.localised_filesize(int(value)) + try: + value = formatters.localised_filesize(int(value)) + except ValueError: + # Sometimes values that can't be converted to ints can sneak + # into the db. In this case, just leave them as they are. + pass elif isinstance(value, basestring): # check if strings are actually datetime/number etc if re.search(reg_ex_datetime, value): @@ -1474,7 +1486,7 @@ def format_resource_items(items): return sorted(output, key=lambda x: x[0]) -def resource_preview(resource, pkg_id): +def resource_preview(resource, package): ''' Returns a rendered snippet for a embedded resource preview. @@ -1483,30 +1495,22 @@ def resource_preview(resource, pkg_id): that embeds a web page, recline or a pdf preview. ''' - format_lower = resource['format'].lower() - directly = False - url = '' - - data_dict = {'resource': resource, 'package': c.package} - if not resource['url']: return snippet("dataviewer/snippets/no_preview.html", resource_type=format_lower, reason=_(u'The resource url is not specified.')) - direct_embed = config.get('ckan.preview.direct', '').split() - if not direct_embed: - direct_embed = datapreview.DEFAULT_DIRECT_EMBED - loadable_in_iframe = config.get('ckan.preview.loadable', '').split() - if not loadable_in_iframe: - loadable_in_iframe = datapreview.DEFAULT_LOADABLE_IFRAME - - if datapreview.can_be_previewed(data_dict): + + format_lower = datapreview.res_format(resource) + directly = False + data_dict = {'resource': resource, 'package': package} + + if datapreview.get_preview_plugin(data_dict, return_first=True): url = url_for(controller='package', action='resource_datapreview', - resource_id=resource['id'], id=pkg_id, qualified=True) - elif format_lower in direct_embed: + resource_id=resource['id'], id=package['id'], qualified=True) + elif format_lower in datapreview.direct(): directly = True url = resource['url'] - elif format_lower in loadable_in_iframe: + elif format_lower in datapreview.loadable(): url = resource['url'] else: reason = None @@ -1582,6 +1586,7 @@ def SI_number_span(number): 'subnav_named_route', 'default_group_type', 'check_access', + 'get_action', 'linked_user', 'group_name_to_title', 'markdown_extract', diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py index ca6c1682418..0d2d8e49499 100644 --- a/ckan/lib/i18n.py +++ b/ckan/lib/i18n.py @@ -34,7 +34,10 @@ def _get_locales(): locale_order = config.get('ckan.locale_order', '').split() locales = ['en'] - i18n_path = os.path.dirname(ckan.i18n.__file__) + if config.get('ckan.i18n_directory'): + i18n_path = os.path.join(config.get('ckan.i18n_directory'), 'i18n') + else: + i18n_path = os.path.dirname(ckan.i18n.__file__) locales += [l for l in os.listdir(i18n_path) if localedata.exists(l)] assert locale_default in locales, \ diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py index 7acc1e39c31..f60b928f434 100644 --- a/ckan/lib/plugins.py +++ b/ckan/lib/plugins.py @@ -192,6 +192,7 @@ def setup_template_variables(self, context, data_dict): c.groups_available = authz_fn(context, data_dict) c.licenses = [('', '')] + base.model.Package.get_license_options() + # CS: bad_spelling ignore 2 lines c.licences = c.licenses deprecate_context_item('licences', 'Use `c.licenses` instead') c.is_sysadmin = ckan.new_authz.is_sysadmin(c.user) diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index de254f14d3f..2c608df77af 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -3,7 +3,7 @@ import logging from pylons import config -from paste.deploy.converters import asbool +import paste.deploy.converters import ckan.lib.plugins as lib_plugins import ckan.logic as logic @@ -181,7 +181,7 @@ def package_create(context, data_dict): context["package"] = pkg ## this is added so that the rest controller can make a new location context["id"] = pkg.id - log.debug('Created object %s' % str(pkg.name)) + log.debug('Created object %s' % pkg.name) # Make sure that a user provided schema is not used on package_show context.pop('schema', None) @@ -575,7 +575,7 @@ def _group_or_org_create(context, data_dict, is_org=False): } logic.get_action('member_create')(member_create_context, member_dict) - log.debug('Created object %s' % str(group.name)) + log.debug('Created object %s' % group.name) return model_dictize.group_dictize(group, context) @@ -822,7 +822,7 @@ def user_create(context, data_dict): context['user_obj'] = user context['id'] = user.id - log.debug('Created user %s' % str(user.name)) + log.debug('Created user %s' % user.name) return user_dict ## Modifications for rest api @@ -888,7 +888,7 @@ def vocabulary_create(context, data_dict): if not context.get('defer_commit'): model.repo.commit() - log.debug('Created Vocabulary %s' % str(vocabulary.name)) + log.debug('Created Vocabulary %s' % vocabulary.name) return model_dictize.vocabulary_dictize(vocabulary, context) @@ -914,7 +914,8 @@ def activity_create(context, activity_dict, ignore_auth=False): :rtype: dictionary ''' - if not asbool(config.get('ckan.activity_streams_enabled', 'true')): + if not paste.deploy.converters.asbool( + config.get('ckan.activity_streams_enabled', 'true')): return model = context['model'] @@ -1141,10 +1142,41 @@ def _group_or_org_member_create(context, data_dict, is_org=False): logic.get_action('member_create')(member_create_context, member_dict) def group_member_create(context, data_dict): + '''Make a user a member of a group. + + You must be authorized to edit the group. + + :param id: the id or name of the group + :type id: string + :param username: name or id of the user to be made member of the group + :type username: string + :param role: role of the user in the group. One of ``member``, ``editor``, + or ``admin`` + :type role: string + + :returns: the newly created (or updated) membership + :rtype: dictionary + ''' _check_access('group_member_create', context, data_dict) return _group_or_org_member_create(context, data_dict) def organization_member_create(context, data_dict): + '''Make a user a member of an organization. + + You must be authorized to edit the organization. + + :param id: the id or name of the organization + :type id: string + :param username: name or id of the user to be made member of the + organization + :type username: string + :param role: role of the user in the organization. One of ``member``, + ``editor``, or ``admin`` + :type role: string + + :returns: the newly created (or updated) membership + :rtype: dictionary + ''' _check_access('organization_member_create', context, data_dict) return _group_or_org_member_create(context, data_dict, is_org=True) diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index 53cf922b0e0..c29f17e4d0c 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -499,7 +499,8 @@ def _group_or_org_member_delete(context, data_dict=None): group_id = data_dict.get('id') group = model.Group.get(group_id) - user_id = data_dict.get('user_id') + user_id = data_dict.get('username') + user_id = data_dict.get('user_id') if user_id is None else user_id member_dict = { 'id': group.id, 'object': user_id, @@ -514,9 +515,29 @@ def _group_or_org_member_delete(context, data_dict=None): def group_member_delete(context, data_dict=None): + '''Remove a user from a group. + + You must be authorized to edit the group. + + :param id: the id or name of the group + :type id: string + :param username: name or id of the user to be removed + :type username: string + + ''' return _group_or_org_member_delete(context, data_dict) def organization_member_delete(context, data_dict=None): + '''Remove a user from an organization. + + You must be authorized to edit the organization. + + :param id: the id or name of the organization + :type id: string + :param username: name or id of the user to be removed + :type username: string + + ''' return _group_or_org_member_delete(context, data_dict) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 407fe81ec05..178313ad3c4 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -69,16 +69,17 @@ def package_list(context, data_dict): ''' model = context["model"] api = context.get("api_version", 1) - ref_package_by = 'id' if api == 2 else 'name' _check_access('package_list', context, data_dict) - query = model.Session.query(model.PackageRevision) - query = query.filter(model.PackageRevision.state=='active') - query = query.filter(model.PackageRevision.current==True) - - packages = query.all() - return [getattr(p, ref_package_by) for p in packages] + package_revision_table = model.package_revision_table + col = (package_revision_table.c.id + if api == 2 else package_revision_table.c.name) + query = _select([col]) + query = query.where(_and_(package_revision_table.c.state=='active', + package_revision_table.c.current==True)) + query = query.order_by(col) + return list(zip(*query.execute())[0]) def current_package_list_with_resources(context, data_dict): '''Return a list of the site's datasets (packages) and their resources. @@ -639,7 +640,7 @@ def user_list(context, data_dict): ) if q: - query = model.User.search(q, query) + query = model.User.search(q, query, user_name=context.get('user')) if order_by == 'edits': query = query.order_by(_desc( @@ -670,12 +671,13 @@ def user_list(context, data_dict): def package_relationships_list(context, data_dict): '''Return a dataset (package)'s relationships. - :param id: the id or name of the package + :param id: the id or name of the first package + :type id: string + :param id2: the id or name of the second package :type id: string - :param id2: - :type id2: - :param rel: - :type rel: + :param rel: relationship as string see + :func:`ckan.logic.action.create.package_relationship_create()` for the + relationship types (optional) :rtype: list of dictionaries @@ -756,6 +758,7 @@ def package_show(context, data_dict): return package_dict + def resource_show(context, data_dict): '''Return the metadata of a resource. @@ -981,6 +984,9 @@ def user_show(context, data_dict): user_dict = model_dictize.user_dictize(user_obj,context) + if context.get('return_minimal'): + return user_dict + revisions_q = model.Session.query(model.Revision ).filter_by(author=user_obj.name) @@ -2795,4 +2801,12 @@ def _unpick_search(sort, allowed_fields=None, total=None): def member_roles_list(context, data_dict): + '''Return the possible roles for members of groups and organizations. + + :returns: a list of dictionaries each with two keys: "text" (the display + name of the role, e.g. "Admin") and "value" (the internal name of the + role, e.g. "admin") + :rtype: list of dictionaries + + ''' return new_authz.roles_list() diff --git a/ckan/logic/action/update.py b/ckan/logic/action/update.py index ad90f86fab5..889bfe5ad4e 100644 --- a/ckan/logic/action/update.py +++ b/ckan/logic/action/update.py @@ -207,31 +207,26 @@ def resource_update(context, data_dict): raise NotFound(_('Resource was not found.')) _check_access('resource_update', context, data_dict) + del context["resource"] - if 'schema' in context: - schema = context['schema'] - else: - package_plugin = lib_plugins.lookup_package_plugin( - resource.resource_group.package.type) - schema = package_plugin.update_package_schema()['resources'] - - data, errors = _validate(data_dict, schema, context) - if errors: - model.Session.rollback() - raise ValidationError(errors) + package_id = resource.resource_group.package.id + pkg_dict = _get_action('package_show')(context, {'id': package_id}) - rev = model.repo.new_revision() - rev.author = user - if 'message' in context: - rev.message = context['message'] + for n, p in enumerate(pkg_dict['resources']): + if p['id'] == id: + break else: - rev.message = _(u'REST API: Update object %s') % data.get("name", "") + logging.error('Could not find resource ' + id) + raise NotFound(_('Resource was not found.')) + pkg_dict['resources'][n] = data_dict - resource = model_save.resource_dict_save(data, context) - if not context.get('defer_commit'): - model.repo.commit() - return model_dictize.resource_dictize(resource, context) + try: + pkg_dict = _get_action('package_update')(context, pkg_dict) + except ValidationError, e: + errors = e.error_dict['resources'][n] + raise ValidationError(errors) + return pkg_dict['resources'][n] def package_update(context, data_dict): @@ -321,7 +316,7 @@ def package_update(context, data_dict): if not context.get('defer_commit'): model.repo.commit() - log.debug('Updated object %s' % str(pkg.name)) + log.debug('Updated object %s' % pkg.name) return_id_only = context.get('return_id_only', False) @@ -764,11 +759,10 @@ def term_translation_update_many(context, data_dict): ''' model = context['model'] - - if not data_dict.get('data') and isinstance(data_dict, list): + if not (data_dict.get('data') and isinstance(data_dict.get('data'), list)): raise ValidationError( - {'error': - 'term_translation_update_many needs to have a list of dicts in field data'} + {'error': 'term_translation_update_many needs to have a ' + 'list of dicts in field data'} ) context['defer_commit'] = True diff --git a/ckan/logic/auth/create.py b/ckan/logic/auth/create.py index 1baf9b61077..bf9c3d17ea3 100644 --- a/ckan/logic/auth/create.py +++ b/ckan/logic/auth/create.py @@ -4,24 +4,32 @@ from ckan.common import _ +@logic.auth_sysadmins_check def package_create(context, data_dict=None): user = context['user'] if not new_authz.auth_is_registered_user(): check1 = new_authz.check_config_permission('anon_create_dataset') else: check1 = new_authz.check_config_permission('create_dataset_if_not_in_organization') \ + or new_authz.check_config_permission('create_unowned_dataset') \ or new_authz.has_user_permission_for_some_org(user, 'create_dataset') if not check1: return {'success': False, 'msg': _('User %s not authorized to create packages') % user} - else: - check2 = _check_group_auth(context,data_dict) - if not check2: - return {'success': False, 'msg': _('User %s not authorized to edit these groups') % str(user)} + check2 = _check_group_auth(context,data_dict) + if not check2: + return {'success': False, 'msg': _('User %s not authorized to edit these groups') % user} + # If an organization is given are we able to add a dataset to it? + data_dict = data_dict or {} + org_id = data_dict.get('organization_id') + if org_id and not new_authz.has_user_permission_for_group_or_org( + org_id, user, 'create_dataset'): + return {'success': False, 'msg': _('User %s not authorized to add dataset to this organization') % user} return {'success': True} + def file_upload(context, data_dict=None): user = context['user'] if not new_authz.auth_is_registered_user(): diff --git a/ckan/logic/auth/delete.py b/ckan/logic/auth/delete.py index fd24b74afe2..61b44697407 100644 --- a/ckan/logic/auth/delete.py +++ b/ckan/logic/auth/delete.py @@ -10,7 +10,7 @@ def package_delete(context, data_dict): authorized = new_authz.has_user_permission_for_group_or_org(package.owner_org, user, 'delete_dataset') if not authorized: - return {'success': False, 'msg': _('User %s not authorized to delete package %s') % (str(user),package.id)} + return {'success': False, 'msg': _('User %s not authorized to delete package %s') % (user, package.id)} else: return {'success': True} @@ -32,7 +32,7 @@ def resource_delete(context, data_dict): authorized = package_delete(context, pkg_dict).get('success') if not authorized: - return {'success': False, 'msg': _('User %s not authorized to delete resource %s') % (str(user), resource.id)} + return {'success': False, 'msg': _('User %s not authorized to delete resource %s') % (user, resource.id)} else: return {'success': True} @@ -130,7 +130,7 @@ def _group_or_org_member_delete(context, data_dict): authorized = new_authz.has_user_permission_for_group_or_org( group.id, user, 'delete_member') if not authorized: - return {'success': False, 'msg': _('User %s not authorized to delete organization %s members') % (str(user),group.id)} + return {'success': False, 'msg': _('User %s not authorized to delete organization %s members') % (user, group.id)} else: return {'success': True} return {'success': True} diff --git a/ckan/logic/auth/get.py b/ckan/logic/auth/get.py index a088289f819..cc310ff3296 100644 --- a/ckan/logic/auth/get.py +++ b/ckan/logic/auth/get.py @@ -133,7 +133,7 @@ def resource_show(context, data_dict): authorized = package_show(context, pkg_dict).get('success') if not authorized: - return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (str(user), resource.id)} + return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (user, resource.id)} else: return {'success': True} diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index b21dd71b647..c153eee84a2 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -21,6 +21,7 @@ duplicate_extras_key, ignore_not_package_admin, ignore_not_group_admin, + ignore_not_sysadmin, no_http, tag_not_uppercase, user_name_validator, @@ -384,6 +385,7 @@ def default_user_schema(): 'about': [ignore_missing, user_about_validator, unicode], 'created': [ignore], 'openid': [ignore_missing], + 'sysadmin': [ignore_missing, ignore_not_sysadmin], 'apikey': [ignore], 'reset_key': [ignore], 'activity_streams_email_notifications': [ignore_missing], diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index 245a635e9ea..da96335fec8 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -110,7 +110,7 @@ def package_name_exists(value, context): result = session.query(model.Package).filter_by(name=value).first() if not result: - raise Invalid(_('Not found') + ': %r' % str(value)) + raise Invalid(_('Not found') + ': %s' % value) return value def package_id_or_name_exists(package_id_or_name, context): @@ -267,7 +267,7 @@ def object_id_validator(key, activity_dict, errors, context): return object_id_validators[activity_type](object_id, context) else: raise Invalid('There is no object_id validator for ' - 'activity type "%s"' % str(activity_type)) + 'activity type "%s"' % activity_type) def extras_unicode_convert(extras, context): for extra in extras: @@ -437,6 +437,19 @@ def ignore_not_package_admin(key, data, errors, context): return data.pop(key) + +def ignore_not_sysadmin(key, data, errors, context): + '''Ignore the field if user not sysadmin or ignore_auth in context.''' + + user = context.get('user') + ignore_auth = context.get('ignore_auth') + + if ignore_auth or (user and new_authz.is_sysadmin(user)): + return + + data.pop(key) + + def ignore_not_group_admin(key, data, errors, context): '''Ignore if the user is not allowed to administer for the group specified.''' diff --git a/ckan/model/license.py b/ckan/model/license.py index 1b65d6040ec..f828edcae69 100644 --- a/ckan/model/license.py +++ b/ckan/model/license.py @@ -184,7 +184,7 @@ class LicenseOpenDataCommonsPDDL(DefaultLicense): @property def title(self): - return _("Open Data Commons Public Domain Dedication and Licence (PDDL)") + return _("Open Data Commons Public Domain Dedication and License (PDDL)") class LicenseOpenDataCommonsOpenDatabase(DefaultLicense): domain_data = True @@ -279,10 +279,12 @@ class LicenseOpenGovernment(DefaultLicense): domain_content = True id = "uk-ogl" is_okd_compliant = True + # CS: bad_spelling ignore url = "http://reference.data.gov.uk/id/open-government-licence" @property def title(self): + # CS: bad_spelling ignore return _("UK Open Government Licence (OGL)") class LicenseCreativeCommonsNonCommercial(DefaultLicense): diff --git a/ckan/model/user.py b/ckan/model/user.py index 7b3c9669aeb..d3eb3d6f1c1 100644 --- a/ckan/model/user.py +++ b/ckan/model/user.py @@ -199,18 +199,24 @@ def get_groups(self, group_type=None, capacity=None): return groups @classmethod - def search(cls, querystr, sqlalchemy_query=None): + def search(cls, querystr, sqlalchemy_query=None, user_name=None): '''Search name, fullname, email and openid. ''' if sqlalchemy_query is None: query = meta.Session.query(cls) else: query = sqlalchemy_query qstr = '%' + querystr + '%' - query = query.filter(or_( + filters = [ cls.name.ilike(qstr), - cls.fullname.ilike(qstr), cls.openid.ilike(qstr), - cls.email.ilike(qstr) - )) + cls.fullname.ilike(qstr), + cls.openid.ilike(qstr), + ] + # sysadmins can search on user emails + import ckan.new_authz as new_authz + if user_name and new_authz.is_sysadmin(user_name): + filters.append(cls.email.ilike(qstr)) + + query = query.filter(or_(*filters)) return query meta.mapper(User, user_table, diff --git a/ckan/new_authz.py b/ckan/new_authz.py index 4552a518a75..1d391c50900 100644 --- a/ckan/new_authz.py +++ b/ckan/new_authz.py @@ -23,6 +23,7 @@ def clear_auth_functions_cache(): def clean_action_name(action_name): ''' Used to convert old style action names into new style ones ''' new_action_name = re.sub('package', 'dataset', action_name) + # CS: bad_spelling ignore return re.sub('licence', 'license', new_action_name) diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py index a37d3c96ae8..8acfc34aad6 100644 --- a/ckan/plugins/interfaces.py +++ b/ckan/plugins/interfaces.py @@ -202,8 +202,20 @@ class IResourcePreview(Interface): def can_preview(self, data_dict): ''' - Return True if the extension can preview the resource. The ``data_dict`` - contains the resource and the package. + Returns info on whether the plugin can preview the resource. + + This can be done in two ways. + The old way is to just return True or False. + The new way is to return a dict with the following + { + 'can_preview': bool - if the extension can preview the resource + 'fixable': string - if the extension cannot preview but could for + example if the resource_proxy was enabled. + 'quality': int - how good the preview is 1-poor, 2-average, 3-good + used if multiple extensions can preview + } + + The ``data_dict`` contains the resource and the package. Make sure to ckeck the ``on_same_domain`` value of the resource or the url if your preview requires the resource to be on diff --git a/ckan/plugins/toolkit.py b/ckan/plugins/toolkit.py index 25882e9395a..75fb586196f 100644 --- a/ckan/plugins/toolkit.py +++ b/ckan/plugins/toolkit.py @@ -51,6 +51,14 @@ class _Toolkit(object): 'ValidationError', # model update validation error 'CkanCommand', # class for providing cli interfaces 'DefaultDatasetForm', # base class for IDatasetForm plugins + 'response', # response object for cookies etc + 'BaseController', # Allow controllers to be created + 'abort', # abort actions + 'redirect_to', # allow redirections + 'url_for', # create urls + 'get_or_bust', # helpful for actions + 'side_effect_free', # actions can be accessed via api + 'auth_sysadmins_check', # allow auth functions to be checked for sysadmins ## Fully defined in this file ## 'add_template_directory', @@ -71,9 +79,11 @@ def _initialize(self): import ckan import ckan.lib.base as base import ckan.logic as logic + import ckan.lib.helpers as h import ckan.lib.cli as cli import ckan.lib.plugins as lib_plugins import ckan.common as common + import pylons # Allow class access to these modules self.__class__.ckan = ckan @@ -105,6 +115,15 @@ def _initialize(self): t['CkanCommand'] = cli.CkanCommand t['DefaultDatasetForm'] = lib_plugins.DefaultDatasetForm + t['response'] = pylons.response + t['BaseController'] = base.BaseController + t['abort'] = base.abort + t['redirect_to'] = h.redirect_to + t['url_for'] = h.url_for + t['get_or_bust'] = logic.get_or_bust + t['side_effect_free'] = logic.side_effect_free + t['auth_sysadmins_check'] = logic.auth_sysadmins_check + # class functions t['render_snippet'] = self._render_snippet t['add_template_directory'] = self._add_template_directory diff --git a/ckan/public/base/css/main.css b/ckan/public/base/css/main.css index 074b5a48385..db3d09d16b1 100644 --- a/ckan/public/base/css/main.css +++ b/ckan/public/base/css/main.css @@ -4938,6 +4938,11 @@ a.tag:hover { margin-bottom: 0; border-top: 1px solid #dddddd; } +.module-content .pagination { + margin-left: -25px; + margin-right: -25px; + margin-bottom: -20px; +} .module .pagination > ul { -webkit-border-radius: 0; -moz-border-radius: 0; @@ -6128,134 +6133,6 @@ textarea { .dataset-heading .popular { top: 0; } -.results { - margin-bottom: 20px; - padding-bottom: 25px; - border-bottom: 1px dotted #dddddd; -} -.results strong, -.is-search-title { - display: block; - font-size: 24px; - line-height: 1.3; - color: #000000; - margin-bottom: 10px; -} -.is-search-title { - margin-bottom: 20px; -} -.results strong:before, -.is-search-title:before { - float: right; - content: " "; - width: 280px; - white-space: pre; -} -.filter-list { - color: #444444; - line-height: 32px; -} -.filter-list .pill { - line-height: 21px; -} -.filter-list .extra { - margin-top: 10px; - font-size: 18px; - font-weight: normal; - color: #000000; -} -.dataset-search { - position: relative; -} -.search-giant, -.search-normal { - position: relative; -} -.search-normal { - display: block; - margin-bottom: 0; -} -.search-giant input { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - font-size: 16px; - padding: 14px 10px; - width: 100%; - height: auto; -} -.search-normal input { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - width: 100%; - height: auto; -} -.search-normal button { - cursor: pointer; - position: absolute; - right: 5px; - top: 50%; - background: transparent; - border: none; - color: #999; - margin-top: -17px; -} -.search-normal button span { - display: none; -} -.search-normal button:hover { - color: #000; -} -.search-giant button { - cursor: pointer; - position: absolute; - right: 15px; - top: 50%; - display: block; - border: none; - padding: 0; - margin-top: -17px; - width: 30px; - height: 30px; - background: transparent url("../../../base/images/icon-search-27x26.png") no-repeat center center; - text-indent: -900em; -} -.control-order-by { - position: absolute; - bottom: -73px; - right: 0; -} -.control-order-by label, -.control-order-by select { - display: inline; -} -.control-order-by select { - width: 160px; -} -.search-aside .control-order-by { - clear: both; - overflow: hidden; - display: block; - position: relative; - bottom: 0; -} -.search-aside .control-order-by label { - float: left; - font-weight: normal; - font-size: 12px; - line-height: 20px; -} -.search-aside .control-order-by select { - float: left; - padding: 2px 4px; - margin: 0; - width: inherit; - font-size: 12px; - height: 20px; - line-height: 20px; - width: 120px; -} .resource-list { margin: 0; list-style: none; @@ -6390,6 +6267,111 @@ textarea { .label[data-format*=turtle] { background-color: #0b4498; } +.search-form { + margin-bottom: 20px; + padding-bottom: 25px; + border-bottom: 1px dotted #dddddd; +} +.search-form .search-input { + position: relative; + margin-bottom: 20px; +} +.search-form .search-input input { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + margin: 0; + width: 100%; + height: auto; +} +.search-form .search-input button { + cursor: pointer; + display: block; + position: absolute; + top: 50%; + margin-top: -10px; + right: 10px; + height: 20px; + padding: 0; + border: none; + background: transparent; +} +.search-form .search-input button span { + display: none; +} +.search-form .search-input button i { + color: #cccccc; + -webkit-transition: color 0.2s ease-in; + -moz-transition: color 0.2s ease-in; + -o-transition: color 0.2s ease-in; + transition: color 0.2s ease-in; +} +.search-form .search-input button:hover i { + color: #000000; +} +.search-form .search-input.search-giant input { + font-size: 16px; + padding: 15px; +} +.search-form .search-input.search-giant button { + margin-top: -15px; + right: 15px; + height: 30px; +} +.search-form .search-input.search-giant button i { + font-size: 28px; + width: 28px; +} +.search-form .control-order-by { + float: right; + margin: 0 0 0 15px; +} +.search-form .control-order-by label, +.search-form .control-order-by select { + display: inline; +} +.search-form .control-order-by select { + width: 160px; + margin: 0; +} +.search-form h2 { + font-size: 24px; + line-height: 1.3; + color: #000000; + margin-bottom: 0; +} +.search-form .filter-list { + color: #444444; + line-height: 32px; + margin: 10px 0 0 0; +} +.search-form .filter-list .pill { + line-height: 21px; +} +.search-form .filter-list .extra { + margin-top: 10px; + font-size: 18px; + font-weight: normal; + color: #000000; +} +.tertiary .control-order-by { + float: none; + margin: 0; +} +.tertiary .control-order-by label { + display: block; + margin-bottom: 5px; + font-weight: normal; + font-size: 12px; +} +.tertiary .control-order-by select { + display: block; + font-size: 12px; + width: 100%; +} +.tertiary .search-input { + margin-bottom: 10px; +} .group .media-vertical .image { margin: 0 -5px 5px; } @@ -6431,12 +6413,8 @@ textarea { .toolbar:after { clear: both; } -.toolbar .add_action { - margin: 0; - list-style: none; - position: absolute; - top: 1px; - right: 0; +.page_primary_action { + margin-bottom: 20px; } .toolbar .breadcrumb { *zoom: 1; @@ -6506,24 +6484,35 @@ textarea { display: none; } .page-header { - position: relative; + *zoom: 1; border-bottom: 1px solid #dddddd; background-color: #f6f6f6; - height: 30px; -webkit-border-radius: 0 3px 0 0; -moz-border-radius: 0 3px 0 0; border-radius: 0 3px 0 0; } +.page-header:before, +.page-header:after { + display: table; + content: ""; + line-height: 0; +} +.page-header:after { + clear: both; +} .page-header .nav-tabs { - position: absolute; - left: 20px; - bottom: -1px; - margin-bottom: 0; + float: left; + margin-bottom: -1px; } .page-header .nav-tabs li.active a, .page-header .nav-tabs a:hover { background-color: #ffffff; } +.page-header .content_action { + float: right; + margin-top: -5px; + margin-right: -7px; +} h1 { font-size: 28px; } @@ -7812,8 +7801,8 @@ h4 small { } .primary .primary { float: left; - width: 479px; - margin-left: 20px; + width: 467px; + margin-left: 0; margin-bottom: 20px; } .primary .primary h1:first-child, @@ -7935,6 +7924,9 @@ h4 small { .context-info .nums dl dd .small { font-size: 21px; } +.context-info .follow_button { + margin-top: 15px; +} .context-info.editing .module-content { margin-top: 0; } @@ -7947,6 +7939,9 @@ h4 small { position: relative; padding-bottom: 0; } +.hero .search-giant { + margin-bottom: 10px; +} .hero .search-giant input { border-color: #003f52; } @@ -7965,10 +7960,13 @@ h4 small { -moz-border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0; background-color: #005d7a; + border-bottom: none; } .hero .module-dark .module-content .heading { margin-top: 0; margin-bottom: 7px; + font-size: 24px; + line-height: 40px; } .hero .tags { *zoom: 1; diff --git a/ckan/public/base/less/ckan.less b/ckan/public/base/less/ckan.less index bb30be4356e..f7c108f8734 100644 --- a/ckan/public/base/less/ckan.less +++ b/ckan/public/base/less/ckan.less @@ -6,6 +6,7 @@ @import "nav.less"; @import "forms.less"; @import "dataset.less"; +@import "search.less"; @import "group.less"; @import "toolbar.less"; @import "prose.less"; diff --git a/ckan/public/base/less/dataset.less b/ckan/public/base/less/dataset.less index d4f4feb2e2d..29b07d6b8a1 100644 --- a/ckan/public/base/less/dataset.less +++ b/ckan/public/base/less/dataset.less @@ -53,152 +53,6 @@ top: 0; } -.results { - margin-bottom: 20px; - padding-bottom: 25px; - border-bottom: 1px dotted @genericBorderColor; -} - -.results strong, -.is-search-title { - display: block; - font-size: 24px; - line-height: 1.3; - color: @layoutBoldColor; - margin-bottom: 10px; -} - -.is-search-title { - margin-bottom: 20px; -} - -// Use a before block to space out the area occupied by the sort select box -// this allows the text content in the strong tag to flow correctly around -// the input. -.results strong:before, -.is-search-title:before { - float: right; - content: " "; - width: 280px; - white-space: pre; -} - -.filter-list { - color: @layoutTextColor; - line-height: 32px; - .pill { - line-height: 21px; - } -} - -.filter-list .extra { - margin-top: 10px; - font-size: 18px; - font-weight: normal; - color: @layoutBoldColor; -} - -.dataset-search { - position: relative; -} - -.search-giant, -.search-normal { - position: relative; -} - -.search-normal { - display: block; - margin-bottom: 0; -} - -.search-giant input { - .box-sizing(border-box); - font-size: 16px; - padding: 14px 10px; - width: 100%; - height: auto; -} - -.search-normal input { - .box-sizing(border-box); - width: 100%; - height: auto; -} - -.search-normal button { - cursor: pointer; - position: absolute; - right: 5px; - top: 50%; - background: transparent; - border: none; - color: #999; - margin-top: -17px; - span { - display: none; - } - &:hover { - color: #000; - } -} - -.search-giant button { - cursor: pointer; - position: absolute; - right: 15px; - top: 50%; - display: block; - border: none; - padding: 0; - margin-top: -17px; - width: 30px; - height: 30px; - background: transparent url("@{imagePath}/icon-search-27x26.png") no-repeat center center; - text-indent: -900em; -} - -.control-order-by { - position: absolute; - bottom: -73px; - right: 0; -} - -.control-order-by label, -.control-order-by select { - display: inline; -} - -.control-order-by select { - width: 160px; -} - -.search-aside { - .control-order-by { - clear: both; - overflow: hidden; - display: block; - position: relative; - bottom: 0; - label { - float: left; - font-weight: normal; - font-size: 12px; - line-height: 20px; - } - select { - float: left; - padding: 2px 4px; - margin: 0; - width: inherit; - font-size: 12px; - height: 20px; - line-height: 20px; - width: 120px; - } - } -} - // Resource List .resource-list { diff --git a/ckan/public/base/less/homepage.less b/ckan/public/base/less/homepage.less index cb5f0eb1188..0aaa8a839ab 100644 --- a/ckan/public/base/less/homepage.less +++ b/ckan/public/base/less/homepage.less @@ -6,8 +6,11 @@ position: relative; padding-bottom: 0; } - .search-giant input { - border-color: darken(@mastheadBackgroundColorEnd, 5); + .search-giant { + margin-bottom: 10px; + input { + border-color: darken(@mastheadBackgroundColorEnd, 5); + } } .page-heading { font-size: 18px; @@ -21,9 +24,12 @@ .module-content { .border-radius(3px 3px 0 0); background-color: @mastheadBackgroundColor; + border-bottom: none; .heading { margin-top: 0; margin-bottom: 7px; + font-size: 24px; + line-height: 40px; } } } diff --git a/ckan/public/base/less/layout.less b/ckan/public/base/less/layout.less index 2ff142b485a..78905c89eee 100644 --- a/ckan/public/base/less/layout.less +++ b/ckan/public/base/less/layout.less @@ -46,8 +46,8 @@ .primary { .primary { float: left; - width: 479px; - margin-left: 20px; + width: 467px; + margin-left: 0; margin-bottom: 20px; h1, h2, h3, h4 { &:first-child { @@ -153,6 +153,9 @@ } } } + .follow_button { + margin-top: 15px; + } &.editing { .module-heading { diff --git a/ckan/public/base/less/module.less b/ckan/public/base/less/module.less index 056569ae10c..20ac16702e7 100644 --- a/ckan/public/base/less/module.less +++ b/ckan/public/base/less/module.less @@ -69,6 +69,12 @@ border-top: 1px solid @moduleHeadingBorderColor; } +.module-content .pagination { + margin-left: -25px; + margin-right: -25px; + margin-bottom: -20px; +} + .module .pagination > ul { .border-radius(0); .box-shadow(none); diff --git a/ckan/public/base/less/search.less b/ckan/public/base/less/search.less new file mode 100644 index 00000000000..a9e4193eb83 --- /dev/null +++ b/ckan/public/base/less/search.less @@ -0,0 +1,108 @@ +.search-form { + // .clearfix; + margin-bottom: 20px; + padding-bottom: 25px; + border-bottom: 1px dotted @genericBorderColor; + + // Normal search box + .search-input { + position: relative; + margin-bottom: 20px; + input { + .box-sizing(border-box); + margin: 0; + width: 100%; + height: auto; + } + button { + cursor: pointer; + display: block; + position: absolute; + top: 50%; + margin-top: -10px; + right: 10px; + height: 20px; + padding: 0; + border: none; + background: transparent; + span { + display: none; + } + i { + color: @inputBorder; + .transition(color 0.2s ease-in); + } + &:hover i { + color: @inputColor; + } + } + &.search-giant { + input { + font-size: 16px; + padding: 15px; + } + button { + margin-top: -15px; + right: 15px; + height: 30px; + i { + font-size: 28px; + width: 28px; + } + } + } + } + .control-order-by { + float: right; + margin: 0 0 0 15px; + label, + select { + display: inline; + } + select { + width: 160px; + margin: 0; + } + } + h2 { + font-size: 24px; + line-height: 1.3; + color: @layoutBoldColor; + margin-bottom: 0; + } + .filter-list { + color: @layoutTextColor; + line-height: 32px; + margin: 10px 0 0 0; + .pill { + line-height: 21px; + } + .extra { + margin-top: 10px; + font-size: 18px; + font-weight: normal; + color: @layoutBoldColor; + } + } +} + +.tertiary { + .control-order-by { + float: none; + margin: 0; + label { + display: block; + margin-bottom: 5px; + font-weight: normal; + font-size: 12px; + } + select { + display: block; + font-size: 12px; + width: 100%; + } + } + .search-input { + margin-bottom: 10px; + } +} diff --git a/ckan/public/base/less/toolbar.less b/ckan/public/base/less/toolbar.less index 7f43f085d8e..2bc6ac2a0d5 100644 --- a/ckan/public/base/less/toolbar.less +++ b/ckan/public/base/less/toolbar.less @@ -5,11 +5,8 @@ padding: 5px 0; } -.toolbar .add_action { - .unstyled; - position: absolute; - top: 1px; - right: 0; +.page_primary_action { + margin-bottom: 20px; } .toolbar .breadcrumb { @@ -78,19 +75,21 @@ } .page-header { - position: relative; + .clearfix; border-bottom: 1px solid @moduleHeadingBorderColor; background-color: @moduleHeadingBackgroundColor; - height: 30px; .border-radius(0 3px 0 0); .nav-tabs { - position: absolute; - left: 20px; - bottom: -1px; - margin-bottom: 0; + float: left; + margin-bottom: -1px; li.active a, a:hover { background-color: @moduleBackgroundColor; } } + .content_action { + float: right; + margin-top: -5px; + margin-right: -7px; + } } diff --git a/ckan/templates/admin/base.html b/ckan/templates/admin/base.html index 83427133f4d..aa7f9daf234 100644 --- a/ckan/templates/admin/base.html +++ b/ckan/templates/admin/base.html @@ -5,21 +5,7 @@ {% block breadcrumb_content %} {% endblock %} -{% block primary_content %} -
- {% block page_header %} - - {% endblock %} -
- {% block primary_content_inner %} - {% endblock %} -
-
+{% block content_primary_nav %} + {{ h.build_nav_icon('ckanadmin_index', _('Sysadmins')) }} + {{ h.build_nav_icon('ckanadmin_config', _('Config')) }} {% endblock %} diff --git a/ckan/templates/group/about.html b/ckan/templates/group/about.html index ecacd804264..69259e22ec3 100644 --- a/ckan/templates/group/about.html +++ b/ckan/templates/group/about.html @@ -1,20 +1,18 @@ -{% extends "group/read.html" %} +{% extends "group/read_base.html" %} {% block subtitle %}{{ _('About') }} - {{ c.group_dict.display_name }}{% endblock %} {% block primary_content_inner %} -
-

{% block page_heading %}{{ c.group_dict.display_name }}{% endblock %}

- {% block group_description %} - {% if c.group_dict.description %} - {{ h.render_markdown(c.group_dict.description) }} - {% else %} -

{{ _('There is no description for this group') }}

- {% endif %} - {% endblock %} +

{% block page_heading %}{{ c.group_dict.display_name }}{% endblock %}

+ {% block group_description %} + {% if c.group_dict.description %} + {{ h.render_markdown(c.group_dict.description) }} + {% else %} +

{{ _('There is no description for this group') }}

+ {% endif %} + {% endblock %} - {% block group_extras %} - {% snippet 'snippets/additional_info.html', extras = h.sorted_extras(c.group_dict.extras) %} - {% endblock %} -
+ {% block group_extras %} + {% snippet 'snippets/additional_info.html', extras = h.sorted_extras(c.group_dict.extras) %} + {% endblock %} {% endblock %} diff --git a/ckan/templates/group/activity_stream.html b/ckan/templates/group/activity_stream.html index dc84175cd7e..800ed24db9b 100644 --- a/ckan/templates/group/activity_stream.html +++ b/ckan/templates/group/activity_stream.html @@ -1,12 +1,10 @@ -{% extends "group/read.html" %} +{% extends "group/read_base.html" %} {% block subtitle %}{{ _('Activity Stream') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} {% block primary_content_inner %} -
-

{% block page_heading %}{{ _('Activity Stream') }}{% endblock %}

- {% block activity_stream %} - {{ c.group_activity_stream | safe }} - {% endblock %} -
+

{% block page_heading %}{{ _('Activity Stream') }}{% endblock %}

+ {% block activity_stream %} + {{ c.group_activity_stream | safe }} + {% endblock %} {% endblock %} diff --git a/ckan/templates/group/admins.html b/ckan/templates/group/admins.html index e5049bc0cef..3e0f2c77672 100644 --- a/ckan/templates/group/admins.html +++ b/ckan/templates/group/admins.html @@ -1,4 +1,4 @@ -{% extends "group/read.html" %} +{% extends "group/read_base.html" %} {% block subtitle %}{{ _('Administrators') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} diff --git a/ckan/templates/group/base_form_page.html b/ckan/templates/group/base_form_page.html index a8936362a0d..25f817826c7 100644 --- a/ckan/templates/group/base_form_page.html +++ b/ckan/templates/group/base_form_page.html @@ -1,19 +1,15 @@ -{% extends "group/index.html" %} +{% extends "group/edit_base.html" %} -{% block add_action_content %}{% endblock %} +{% block page_primary_action %}{% endblock %} {% block breadcrumb_content %}
  • {{ h.nav_link(_('Groups'), controller='group', action='index') }}
  • {% block breadcrumb_link %}{{ h.nav_link(_('Add a Group'), controller='group', action='new') }}{% endblock %}
  • {% endblock %} -{% block primary_content %} -
    -
    -

    {% block page_heading %}{{ _('Group Form') }}{% endblock %}

    - {% block form %} - {{ c.form | safe }} - {% endblock %} -
    -
    +{% block primary_content_inner %} +

    {% block page_heading %}{{ _('Group Form') }}{% endblock %}

    + {% block form %} + {{ c.form | safe }} + {% endblock %} {% endblock %} diff --git a/ckan/templates/group/edit.html b/ckan/templates/group/edit.html index 90734091141..57808056d54 100644 --- a/ckan/templates/group/edit.html +++ b/ckan/templates/group/edit.html @@ -1,7 +1,14 @@ {% extends "group/base_form_page.html" %} -{% block subtitle %}{{ _('Edit a Group') }}{% endblock %} +{% block breadcrumb_content %} +
  • {% link_for _('Groups'), controller='group', action='index' %}
  • + {% block breadcrumb_content_inner %} +
  • {% link_for group.display_name|truncate(35), controller='group', action='read', id=group.name %}
  • +
  • {% link_for _('Edit'), controller='group', action='edit', id=group.name %}
  • + {% endblock %} +{% endblock %} -{% block breadcrumb_link %}{% link_for _('Edit Group'), controller='group', action='edit', id=c.group.name %}{% endblock %} +{% block subtitle %}{{ _('Edit Group') }}{% endblock %} -{% block page_heading %}{{ _('Edit a Group') }}{% endblock %} +{% block page_heading_class %}hide-heading{% endblock %} +{% block page_heading %}{{ _('Edit Group') }}{% endblock %} diff --git a/ckan/templates/group/edit_base.html b/ckan/templates/group/edit_base.html new file mode 100644 index 00000000000..a5f1203d529 --- /dev/null +++ b/ckan/templates/group/edit_base.html @@ -0,0 +1,24 @@ +{% extends "page.html" %} + +{% set group = c.group_dict %} + +{% block breadcrumb_content %} +
  • {% link_for _('Groups'), controller='group', action='index' %}
  • + {% block breadcrumb_content_inner %} +
  • {% link_for group.display_name|truncate(35), controller='group', action='read', id=group.name %}
  • +
  • {% link_for _('Edit'), controller='group', action='edit', id=group.name %}
  • + {% endblock %} +{% endblock %} + +{% block content_action %} + {% link_for _('View group'), controller='group', action='read', id=c.group_dict.name, class_='btn', icon='eye-open' %} +{% endblock %} + +{% block content_primary_nav %} + {{ h.build_nav_icon('group_edit', _('Edit'), id=c.group_dict.name) }} + {{ h.build_nav_icon('group_members', _('Members'), id=c.group_dict.name) }} +{% endblock %} + +{% block secondary_content %} + {% snippet "group/snippets/info.html", group=c.group_dict, show_nums=false %} +{% endblock %} diff --git a/ckan/templates/group/followers.html b/ckan/templates/group/followers.html index a4569cd2fc4..f2e4a60dd34 100644 --- a/ckan/templates/group/followers.html +++ b/ckan/templates/group/followers.html @@ -1,4 +1,4 @@ -{% extends "group/read.html" %} +{% extends "group/read_base.html" %} {% block subtitle %}{{ _('Followers') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} diff --git a/ckan/templates/group/index.html b/ckan/templates/group/index.html index 5c137aea788..8db2afb9c31 100644 --- a/ckan/templates/group/index.html +++ b/ckan/templates/group/index.html @@ -6,24 +6,19 @@
  • {% link_for _('Groups'), controller='group', action='index' %}
  • {% endblock %} -{% block add_action_content %} +{% block page_header %}{% endblock %} + +{% block page_primary_action %} {% if h.check_access('group_create') %} {% link_for _('Add Group'), controller='group', action='new', class_='btn btn-primary', icon='plus-sign-alt' %} {% endif %} {% endblock %} -{% block primary_content %} -
    -
    +{% block primary_content_inner %}

    {{ _('Groups') }}

    {% block groups_search_form %} - {% snippet 'snippets/simple_search.html', q=c.q, sort=c.sort_by_selected, placeholder=_('Search groups...') %} + {% snippet 'snippets/search_form.html', type='group', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, placeholder=_('Search groups...'), show_empty=request.params %} {% endblock %} -

    - {% block groups_search_result_text %} - {% snippet 'snippets/search_result_text.html', query=c.q, count=c.page.item_count, type='group' %} - {% endblock %} -

    {% block groups_list %} {% if c.page.items or request.params %} {% snippet "group/snippets/group_list.html", groups=c.page.items %} @@ -36,27 +31,11 @@

    {% endif %} {% endblock %} -

    {% block page_pagination %} {{ c.page.pager() }} {% endblock %} -
    {% endblock %} {% block secondary_content %} -
    -

    - - {{ _('What are Groups?') }} -

    -
    - {% trans %} -

    Groups allow you to group together datasets under a community (for - example, Civil Liberty data) or topic (e.g. Transport, Health, - Environment) to make it easier for users to browse datasets by theme. - Datasets can be part of a group, but do not belong to the group for - editing or authorisation purposes.

    - {% endtrans %} -
    -
    + {% snippet "group/snippets/helper.html" %} {% endblock %} diff --git a/ckan/templates/group/member_new.html b/ckan/templates/group/member_new.html index e801b381b7f..87c6dbb60c2 100644 --- a/ckan/templates/group/member_new.html +++ b/ckan/templates/group/member_new.html @@ -1,48 +1,59 @@ -{% extends "page.html" %} -{% import 'macros/form.html' as form %} - -{% block subtitle %}{{ c.group_dict.display_name }}{% endblock %} +{% extends "group/edit_base.html" %} -{% block breadcrumb_content %} -
  • {% link_for _('Groups'), controller='group', action='index' %}
  • -
  • {% link_for c.group_dict.display_name|truncate(35), controller='group', action='read', id=c.group_dict.name %}
  • -{% endblock %} +{% import 'macros/form.html' as form %} -{% block actions_content %} - {% if h.check_access('organization_member_create', {'id': c.group_dict.id}) %} -
  • {% link_for _('Members'), controller='group', action='members', id=c.group_dict.id, class_='btn' %}
  • - {% endif %} -{% endblock %} +{% set user = c.user_dict %} -{% block primary_content %} -
    -
    -

    - {% block page_heading %}{{ _('Edit Member') if c.user_name else _('Add Member') }}{% endblock %} -

    - {% block form %} -
    - {% if c.user_name %} - - {% else %} - {% set format_attrs = {'data-module': 'autocomplete', 'data-module-source': '/api/2/util/user/autocomplete?q=?'} %} - {{ form.input('username', id='field-username', label=_('User'), placeholder=_('Username'), value='', error='', classes=['control-medium'], attrs=format_attrs) }} - {% endif %} - {% set format_attrs = {'data-module': 'autocomplete'} %} - {{ form.select('role', label=_('Role'), options=c.roles, selected='', error='', attrs=format_attrs) }} - -
    - {% endblock %} +{% block primary_content_inner %} + {% link_for _('Back to all members'), controller='group', action='members', id=group.name, class_='btn pull-right', icon='arrow-left' %} +

    + {% block page_heading %}{{ _('Edit Member') if user else _('Add Member') }}{% endblock %} +

    + {% block form %} +
    + {% if user %} + + {% set format_attrs = {'disabled': true} %} + {{ form.input('username', label=_('User'), value=user.name, classes=['control-medium'], attrs=format_attrs) }} + {% else %} + {% set format_attrs = {'data-module': 'autocomplete', 'data-module-source': '/api/2/util/user/autocomplete?q=?'} %} + {{ form.input('username', id='field-username', label=_('User'), placeholder=_('Username'), value='', error='', classes=['control-medium'], attrs=format_attrs) }} + {% endif %} + {% set format_attrs = {'data-module': 'autocomplete'} %} + {{ form.select('role', label=_('Role'), options=c.roles, selected=c.user_role, error='', attrs=format_attrs) }} +
    + {% if user %} + {% set locale = h.dump_json({'content': _('Are you sure you want to delete this member?')}) %} + {{ _('Delete') }} + + {% else %} + + {% endif %}
    -
    + + {% endblock %} {% endblock %} {% block secondary_content %} - {% snippet 'snippets/group.html', group=c.group_dict %} - -{% endblock %} - -{% block links %} {{ super() }} - {% include "group/snippets/feeds.html" %} +
    +

    + + {{ _('What are roles?') }} +

    +
    + {% trans %} +

    Admin: Can add/edit and delete datasets, as well as + manage group members.

    +

    Editor: Can add and edit datasets, but not manage + group members.

    +

    Member: Can view the group's private + datasets, but not add new datasets.

    + {% endtrans %} +
    +
    {% endblock %} diff --git a/ckan/templates/group/members.html b/ckan/templates/group/members.html index 1f00648f140..600c6d1bf54 100644 --- a/ckan/templates/group/members.html +++ b/ckan/templates/group/members.html @@ -1,44 +1,39 @@ -{% extends "page.html" %} +{% extends "group/edit_base.html" %} -{% block subtitle %}{{ c.group_dict.display_name }}{% endblock %} +{% block subtitle %}{{ _('Members') }} - {{ c.group_dict.display_name }}{% endblock %} -{% block breadcrumb_content %} -
  • {% link_for _('Groups'), controller='group', action='index' %}
  • -
  • {% link_for c.group_dict.display_name|truncate(35), controller='group', action='read', id=c.group_dict.name %}
  • -{% endblock %} - -{% block actions_content %} - {% if h.check_access('organization_member_create', {'id': c.group_dict.id}) %} -
  • {% link_for _('Add Member'), controller='group', action='member_new', id=c.group_dict.id, class_='btn', icon='plus' %}
  • - {% endif %} -{% endblock %} - -{% block primary_content %} -
    -
    -

    {% block page_heading %}{{ _('Members') }}{% endblock %}

    - {% block members_list %} - - {% for user_id, user, role in c.members %} - - - - +{% block primary_content_inner %} + {% link_for _('Add Member'), controller='group', action='member_new', id=c.group_dict.id, class_='btn pull-right', icon='plus-sign-alt' %} +

    {{ _('{0} members'.format(c.members|length)) }}

    +
    {{ h.linked_user(user_id, maxlength=20) }}{{ role }}{% link_for _('Edit'), controller='group', action='member_new', id=c.group_dict.id, class_='btn', user=user_id %}
    + + + + + + + + + + + + {% for user_id, user, role in c.members %} + + + + - - {% endfor %} -
    {{ _('User') }}{{ _('Role') }}
    + {{ h.linked_user(user_id, maxlength=20) }} + {{ role }} {% set locale = h.dump_json({'content': _('Are you sure you want to delete this member?')}) %} - {% block delete_button_text %}{{ _('Delete') }}{% endblock %}
    - {% endblock %} -
    -
    -{% endblock %} - -{% block secondary_content %} - {% snippet 'snippets/group.html', group=c.group_dict %} -{% endblock %} - -{% block links %} - {{ super() }} - {% include "group/snippets/feeds.html" %} +
    + + + + {% block delete_button_text %}{% endblock %} +
    + + + {% endfor %} + + {% endblock %} diff --git a/ckan/templates/group/new.html b/ckan/templates/group/new.html index 276ba723c30..91179be106c 100644 --- a/ckan/templates/group/new.html +++ b/ckan/templates/group/new.html @@ -6,4 +6,9 @@ {% block page_heading %}{{ _('Create a Group') }}{% endblock %} -{% block actions_content %}{% endblock %} +{% block page_header %}{% endblock %} + +{% block secondary_content %} + {% snippet "group/snippets/helper.html" %} +{% endblock %} + diff --git a/ckan/templates/group/read.html b/ckan/templates/group/read.html index f56359b37d9..6d50fd0d905 100644 --- a/ckan/templates/group/read.html +++ b/ckan/templates/group/read.html @@ -2,20 +2,34 @@ {% block subtitle %}{{ c.group_dict.display_name }}{% endblock %} +{% block page_primary_action %} + {% link_for _('Add Dataset'), controller='package', action='new', group=c.group_dict.id, class_='btn btn-primary', icon='plus-sign-alt' %} +{% endblock %} + {% block primary_content_inner %} -
    - {% block packages_list %} - {% include "package/snippets/search_form.html" %} - {% endblock %} -
    + {% block groups_search_form %} + {% set facets = { + 'fields': c.fields_grouped, + 'search': c.search_facets, + 'titles': c.facet_titles, + 'translated_fields': c.translated_fields, + 'remove_field': c.remove_field } + %} + {% snippet 'snippets/search_form.html', type='dataset', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, facets=facets, placeholder=_('Search datasets...'), show_empty=request.params %} + {% endblock %} + {% block packages_list %} + {% if c.page.items %} + {{ h.snippet('snippets/package_list.html', packages=c.page.items) }} + {% endif %} + {% endblock %} {% block page_pagination %} - {{ c.page.pager(q=c.q) }} + {{ c.page.pager(q=c.q) }} {% endblock %} {% endblock %} {% block secondary_content %} {{ super() }} {% for facet in c.facet_titles %} - {{ h.snippet('snippets/facet_list.html', title=c.facet_titles[facet], name=facet, extras={'id':c.group_dict.id}) }} + {{ h.snippet('snippets/facet_list.html', title=c.facet_titles[facet], name=facet, extras={'id':c.group_dict.id}) }} {% endfor %} {% endblock %} diff --git a/ckan/templates/group/read_base.html b/ckan/templates/group/read_base.html index 949c43478f6..afa92f2038f 100644 --- a/ckan/templates/group/read_base.html +++ b/ckan/templates/group/read_base.html @@ -7,60 +7,20 @@
  • {% link_for c.group_dict.display_name|truncate(35), controller='group', action='read', id=c.group_dict.name %}
  • {% endblock %} -{% block actions_content %} +{% block content_action %} {% if h.check_access('group_update', {'id': c.group_dict.id}) %} -
  • {% link_for _('Add Dataset'), controller='package', action='new', group=c.group_dict.id, class_='btn', icon='plus-sign-alt' %}
  • -
  • {% link_for _('Edit'), controller='group', action='edit', id=c.group_dict.name, class_='btn btn-primary', icon='wrench' %}
  • + {% link_for _('Edit'), controller='group', action='edit', id=c.group_dict.name, class_='btn', icon='wrench' %} {% endif %} -
  • {{ h.follow_button('group', c.group_dict.id) }}
  • {% endblock %} -{% block primary_content %} -
    - {% block page_header %} - - {% endblock %} - {% block primary_content_inner %}{% endblock %} -
    +{% block content_primary_nav %} + {{ h.build_nav_icon('group_read', _('Datasets'), id=c.group_dict.name) }} + {{ h.build_nav_icon('group_activity', _('Activity Stream'), id=c.group_dict.name, offset=0) }} + {{ h.build_nav_icon('group_about', _('About'), id=c.group_dict.name) }} {% endblock %} {% block secondary_content %} -
    -
    -
    - - {{ c.group_dict.name }} - -
    -

    {{ c.group_dict.display_name }}

    - {% if c.group_dict.description %} -

    - {{ h.markdown_extract(c.group_dict.description, 180) }} - {% link_for _('read more'), controller='group', action='about', id=c.group_dict.name %} -

    - {% else %} -

    {{ _('There is no description for this group') }}

    - {% endif %} -
    -
    -
    {{ _('Followers') }}
    -
    {{ h.SI_number_span(c.group_dict.num_followers) }}
    -
    -
    -
    {{ _('Datasets') }}
    -
    {{ h.SI_number_span(c.group_dict.packages|length) }}
    -
    -
    -
    -
    + {% snippet "group/snippets/info.html", group=c.group_dict, show_nums=true %} {% endblock %} {% block links %} diff --git a/ckan/templates/group/snippets/helper.html b/ckan/templates/group/snippets/helper.html new file mode 100644 index 00000000000..a73c9d6dde7 --- /dev/null +++ b/ckan/templates/group/snippets/helper.html @@ -0,0 +1,15 @@ +
    +

    + + {{ _('What are Groups?') }} +

    +
    + {% trans %} +

    Groups allow you to group together datasets under a community (for + example, Civil Liberty data) or topic (e.g. Transport, Health, + Environment) to make it easier for users to browse datasets by theme. + Datasets can be part of a group, but do not belong to the group for + editing or authorisation purposes.

    + {% endtrans %} +
    +
    \ No newline at end of file diff --git a/ckan/templates/group/snippets/info.html b/ckan/templates/group/snippets/info.html new file mode 100644 index 00000000000..b7c749ab147 --- /dev/null +++ b/ckan/templates/group/snippets/info.html @@ -0,0 +1,33 @@ +
    +
    +
    + + {{ group.name }} + +
    +

    {{ group.display_name }}

    + {% if group.description %} +

    + {{ h.markdown_extract(group.description, 180) }} + {% link_for _('read more'), controller='group', action='about', id=group.name %} +

    + {% else %} +

    {{ _('There is no description for this group') }}

    + {% endif %} + {% if show_nums %} +
    +
    +
    {{ _('Followers') }}
    +
    {{ h.SI_number_span(group.num_followers) }}
    +
    +
    +
    {{ _('Datasets') }}
    +
    {{ h.SI_number_span(group.packages|length) }}
    +
    +
    + + {% endif %} +
    +
    \ No newline at end of file diff --git a/ckan/templates/header.html b/ckan/templates/header.html index 518375def6e..f684965c7ce 100644 --- a/ckan/templates/header.html +++ b/ckan/templates/header.html @@ -26,17 +26,21 @@ {{ c.new_activities }} + {% block header_account_settings_link %}
  • + {% endblock %} + {% block header_account_log_out_link %}
  • {% endblock %} + {% endblock %} {% else %} diff --git a/ckan/templates/home/index.html b/ckan/templates/home/index.html index 30a0fa6b57d..2508f8b8508 100644 --- a/ckan/templates/home/index.html +++ b/ckan/templates/home/index.html @@ -57,12 +57,15 @@

    {% block home_image_caption %}{{ _("This is a featured {% block home_secondary_content %}
    {% block home_search %} -

    + {% block package_notes %} + {% if c.pkg_notes_formatted %} +
    + {{ c.pkg_notes_formatted }} +
    + {% endif %} + {% endblock %} + {# FIXME why is this here? seems wrong #} + {% endblock %} {% block package_resources %} diff --git a/ckan/templates/package/read_base.html b/ckan/templates/package/read_base.html index ece96487471..bc5b0d25e3d 100644 --- a/ckan/templates/package/read_base.html +++ b/ckan/templates/package/read_base.html @@ -14,29 +14,19 @@ {% endblock -%} -{% block actions_content %} - {# NOTE: Not implemented in stage 1 #} - {#
  • {% link_for _('History'), controller='package', action='history', id=pkg.name, class_='btn', icon='undo' %}
  • #} +{% block content_action %} {% if h.check_access('package_update', {'id':pkg.id }) %} -
  • {% link_for _('Edit'), controller='package', action='edit', id=pkg.name, class_='btn btn-primary', icon='wrench' %}
  • + {% link_for _('Edit'), controller='package', action='edit', id=pkg.name, class_='btn', icon='wrench' %} {% endif %} -
  • {{ h.follow_button('dataset', pkg.id) }}
  • {% endblock %} -{% block primary_content %} -
    - {% block page_header %} - - {% endblock %} +{% block content_primary_nav %} + {{ h.build_nav_icon('dataset_read', _('Dataset'), id=pkg.name) }} + {{ h.build_nav_icon('dataset_activity', _('Activity Stream'), id=pkg.name) }} + {{ h.build_nav_icon('related_list', _('Related'), id=pkg.name) }} +{% endblock %} +{% block primary_content_inner %} {% block package_revision_info %} {% if c.pkg_revision_id %}
    @@ -53,15 +43,29 @@
    {% endif %} {% endblock %} - - {% block primary_content_inner %}{% endblock %} -
    {% endblock %} {% block secondary_content %} {% block secondary_help_content %}{% endblock %} + {% block package_info %} +
    +
    +

    {{ pkg.title or pkg.name }}

    +
    +
    +
    {{ _('Followers') }}
    +
    {{ h.SI_number_span(h.get_action('dataset_follower_count', {'id': pkg.id})) }}
    +
    +
    + +
    +
    + {% endblock %} + {% block package_organization %} {% if pkg.organization %} {% snippet "snippets/organization.html", organization=pkg.organization %} diff --git a/ckan/templates/package/related_list.html b/ckan/templates/package/related_list.html index 1b7dfd5b806..d9aa5ac34f9 100644 --- a/ckan/templates/package/related_list.html +++ b/ckan/templates/package/related_list.html @@ -3,7 +3,6 @@ {% set pkg = c.pkg %} {% block primary_content_inner %} -

    {% block page_heading %}{{ _('Related Media for {dataset}').format(dataset=h.dataset_display_name(c.pkg)) }}{% endblock %}

    {% block related_list %} {% if c.pkg.related %} @@ -17,5 +16,4 @@

    {% block page_heading %}{{ _('Related Media for {datase {% link_for _('Add Related Item'), controller='related', action='new', id=pkg.name, class_='btn btn-primary' %} {% endblock %} -

    {% endblock %} diff --git a/ckan/templates/package/resource_edit.html b/ckan/templates/package/resource_edit.html index fdc6f9001f5..52ac2ab4722 100644 --- a/ckan/templates/package/resource_edit.html +++ b/ckan/templates/package/resource_edit.html @@ -12,8 +12,8 @@
  • {{ _('Edit') }}
  • {% endblock %} -{% block actions_content %} -
  • {% link_for _('View resource'), controller='package', action='resource_read', id=pkg_dict.name, resource_id=res.id, class_='btn', icon='eye-open' %}
  • +{% block content_action %} + {% link_for _('View resource'), controller='package', action='resource_read', id=pkg_dict.name, resource_id=res.id, class_='btn', icon='eye-open' %} {% endblock %} {# logged_in is defined in new_resource.html #} diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html index 441745bfdac..46406746a6f 100644 --- a/ckan/templates/package/resource_read.html +++ b/ckan/templates/package/resource_read.html @@ -36,6 +36,8 @@ {{ _('View') }} {% elif res.resource_type == 'api' %} {{ _('API Endpoint') }} + {% elif not res.can_be_previewed %} + {{ _('Go to resource') }} {% else %} {{ _('Download') }} {% endif %} @@ -71,7 +73,7 @@

    {{ _('From the dataset abstract') }}

    {% block data_preview %} - {{ h.resource_preview(c.resource, c.package.id) }} + {{ h.resource_preview(c.resource, c.package) }} {% endblock %} {% endblock %} diff --git a/ckan/templates/package/search.html b/ckan/templates/package/search.html index 09c5d24c9e3..25a4d80974f 100644 --- a/ckan/templates/package/search.html +++ b/ckan/templates/package/search.html @@ -3,12 +3,6 @@ {% block subtitle %}{{ _("Search for a Dataset") }}{% endblock %} -{% block add_action_content %} - {% if h.check_access('package_create') %} - {% link_for _('Add Dataset'), controller='package', action='new', class_='btn btn-primary', icon='plus-sign-alt' %} - {% endif %} -{% endblock %} - {% block breadcrumb_content %}
  • {{ h.nav_link(_('Datasets'), controller='package', action='search', highlight_actions = 'new index') }}
  • {% endblock %} @@ -16,71 +10,33 @@ {% block primary_content %}
    + {% block page_primary_action %} + {% if h.check_access('package_create') %} +
    + {% link_for _('Add Dataset'), controller='package', action='new', class_='btn btn-primary', icon='plus-sign-alt' %} +
    + {% endif %} + {% endblock %} {% block form %} - + {% set facets = { + 'fields': c.fields_grouped, + 'search': c.search_facets, + 'titles': c.facet_titles, + 'translated_fields': c.translated_fields, + 'remove_field': c.remove_field } + %} + {% set sorting = [ + (_('Relevance'), 'score desc, metadata_modified desc'), + (_('Name Ascending'), 'title_string asc'), + (_('Name Descending'), 'title_string desc'), + (_('Last Modified'), 'metadata_modified desc'), + (_('Popular'), 'views_recent desc') if g.tracking_enabled else (false, false) ] + %} + {% snippet 'snippets/search_form.html', type='dataset', query=c.q, sorting=sorting, sorting_selected=c.sort_by_selected, count=c.page.item_count, facets=facets, show_empty=request.params, error=c.query_error %} {% endblock %} - {% block package_search_results %} -
    - - {% block package_search_results_text %} - {% snippet 'snippets/search_result_text.html', query=c.q, count=c.page.item_count, type='dataset' %} - {% endblock %} - -
    - {% block package_search_results_filter_list %} - {% for field in c.fields_grouped %} - {% set search_facets_items = c.search_facets.get(field)['items'] %} - {{ c.facet_titles.get(field) }}: - {% for value in c.fields_grouped[field] %} - - {%- if c.translated_fields and c.translated_fields.has_key((field,value)) -%} - {{ c.translated_fields[(field,value)] }} - {%- else -%} - {{ h.list_dict_filter(search_facets_items , 'name', 'display_name', value) }} - {%- endif %} - - - {% endfor %} - {% endfor %} - {% endblock %} -
    - {% if request.params and c.page.item_count == 0 %} - {% trans %} -

    Please try another search.

    - {% endtrans %} - {% endif %} -
    - - {% if c.query_error %} - {% trans %} -

    There was an error while searching. Please try again.

    - {% endtrans %} - {% endif %} {% block package_search_results_list %} {{ h.snippet('snippets/package_list.html', packages=c.page.items) }} {% endblock %} - {% endblock %}
    {% block page_pagination %} diff --git a/ckan/templates/package/snippets/additional_info.html b/ckan/templates/package/snippets/additional_info.html index f5e864c7674..2fa73a36ec2 100644 --- a/ckan/templates/package/snippets/additional_info.html +++ b/ckan/templates/package/snippets/additional_info.html @@ -1,4 +1,4 @@ -
    +

    {{ _('Additional Info') }}

    diff --git a/ckan/templates/package/snippets/resource_item.html b/ckan/templates/package/snippets/resource_item.html index 103df3ec28b..fccd94ff4d2 100644 --- a/ckan/templates/package/snippets/resource_item.html +++ b/ckan/templates/package/snippets/resource_item.html @@ -24,14 +24,24 @@ {% block resource_item_explore_links %}
  • - - {{ _('Preview') }} + {% if res.can_be_previewed %} + + {{ _('Preview') }} + {% else %} + + {{ _('More information') }} + {% endif %}
  • - - {{ _('Download') }} + {% if res.can_be_previewed %} + + {{ _('Download') }} + {% else %} + + {{ _('Go to resource') }} + {% endif %}
  • {% endblock %} diff --git a/ckan/templates/package/snippets/resources_list.html b/ckan/templates/package/snippets/resources_list.html index f2327a5a915..7cd33a1c59b 100644 --- a/ckan/templates/package/snippets/resources_list.html +++ b/ckan/templates/package/snippets/resources_list.html @@ -9,7 +9,7 @@ {% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources %} #} -
    +

    {{ _('Data and Resources') }}

    {% block resource_list %} {% if resources %} diff --git a/ckan/templates/package/snippets/search_form.html b/ckan/templates/package/snippets/search_form.html deleted file mode 100644 index 35b01b2b2dc..00000000000 --- a/ckan/templates/package/snippets/search_form.html +++ /dev/null @@ -1,63 +0,0 @@ - - {% block search_form_input %} - - - - - {% endblock %} - {% block search_form_sortby %} - {% snippet 'snippets/sort_by.html', sort=c.sort_by_selected %} - {% endblock %} - {% block search_form_fields %} - {% if c.fields %} - - {% for key, value in c.fields %} - - {% endfor %} - - {% endif %} - {% endblock %} -
    - - -{% block search_form_results %} -
    - - {% block search_form_result_text %} - {% snippet 'snippets/search_result_text.html', query=c.q, count=c.page.item_count, type='dataset' %} - {% endblock %} - - {% block search_form_filter_list %} -
    - {% for field in c.fields_grouped %} - {{ c.facet_titles.get(field) }}: - {% for value in c.fields_grouped[field] %} - - {% if c.translated_fields and c.translated_fields.has_key((field,value)) %} - {{ c.translated_fields[(field,value)] }} - {% else %} - {{ value }} - {% endif %} - - - {% endfor %} - {% endfor %} -
    - {% endblock %} -
    -{% endblock %} - -{% block search_form_package_list %} -{% if c.query_error %} -

    - {% trans %} - There was an error while searching. Please try again. - {% endtrans %} -

    - -{% else %} - {% if c.page.items %} - {{ h.snippet('snippets/package_list.html', packages=c.page.items, bulk_processing=c.bulk_processing) }} - {% endif %} -{% endif %} -{% endblock %} diff --git a/ckan/templates/package/snippets/tags.html b/ckan/templates/package/snippets/tags.html index ab95b97bdc7..8d296d62ea2 100644 --- a/ckan/templates/package/snippets/tags.html +++ b/ckan/templates/package/snippets/tags.html @@ -1,5 +1,5 @@ {% if tags %} -
    +
    {% snippet 'snippets/tag_list.html', tags=tags, _class='tag-list well' %}
    {% endif %} diff --git a/ckan/templates/page.html b/ckan/templates/page.html index 01f50bfd1f7..0f2b3a57885 100644 --- a/ckan/templates/page.html +++ b/ckan/templates/page.html @@ -29,13 +29,6 @@ {% block toolbar %}
    - {% block add_action %} - {% if self.add_action_content() | trim %} -
    - {% block add_action_content %}{% endblock %} -
    - {% endif %} - {% endblock %} {% block breadcrumb %} {% if self.breadcrumb_content() | trim %}
    + {% endif %}
    diff --git a/ckan/templates/snippets/search_form.html b/ckan/templates/snippets/search_form.html new file mode 100644 index 00000000000..6fdd9e1638e --- /dev/null +++ b/ckan/templates/snippets/search_form.html @@ -0,0 +1,80 @@ +{% import 'macros/form.html' as form %} + +{% set placeholder = placeholder if placeholder else _('Search...') %} +{% set sorting = sorting if sorting else [(_('Name Ascending'), 'name asc'), (_('Name Descending'), 'name desc')] %} +{% set search_class = search_class if search_class else 'search-giant' %} + +
    + + {% block search_input %} +
    + + +
    + {% endblock %} + + {% block search_search_fields %} + {% if fields -%} + {{ form.hidden_from_list(fields=fields) }} + {%- endif %} + {% endblock %} + + {% block search_sortby %} + {% if sorting %} +
    + + + +
    + {% endif %} + {% endblock %} + + {% block search_title %} + {% if not no_title %} +

    {% snippet 'snippets/search_result_text.html', query=query, count=count, type=type %}

    + {% endif %} + {% endblock %} + + {% block search_facets %} + {% if facets %} +

    + {% for field in facets.fields %} + {% set search_facets_items = facets.search.get(field)['items'] %} + {{ facets.titles.get(field) }}: + {% for value in facets.fields[field] %} + + {%- if facets.translated_fields and facets.translated_fields.has_key((field,value)) -%} + {{ facets.translated_fields[(field,value)] }} + {%- else -%} + {{ h.list_dict_filter(search_facets_items, 'name', 'display_name', value) }} + {%- endif %} + + + {% endfor %} + {% endfor %} +

    + {% endif %} + {% endblock %} + + + +{% if show_empty and count == 0 %} + {% trans %} +

    Please try another search.

    + {% endtrans %} +{% endif %} + +{% if error %} + {% trans %} +

    There was an error while searching. Please try again.

    + {% endtrans %} +{% endif %} diff --git a/ckan/templates/user/dashboard.html b/ckan/templates/user/dashboard.html index 5ee3e1edffd..d9458910c47 100644 --- a/ckan/templates/user/dashboard.html +++ b/ckan/templates/user/dashboard.html @@ -1,8 +1,8 @@ {% extends "user/edit_base.html" %} -{% block add_action_content %} +{% block page_primary_action %} {% if h.check_access('package_create') %} -
  • {% link_for _('Add Dataset'), controller='package', action='new', class_="btn btn-primary", icon="plus-sign-alt" %}
  • + {% link_for _('Add Dataset'), controller='package', action='new', class_="btn btn-primary", icon="plus-sign-alt" %} {% endif %} {% endblock %} diff --git a/ckan/templates/user/edit_base.html b/ckan/templates/user/edit_base.html index 4826aada4da..f10048b5cd4 100644 --- a/ckan/templates/user/edit_base.html +++ b/ckan/templates/user/edit_base.html @@ -13,7 +13,7 @@ {% block page_header %}