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 %}
- {{ _('There is no description for this group') }} {{ _('There is no description for this group') }}{% 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 %}
- {% 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 %}
+ {% block page_heading %}{{ _('Activity Stream') }}{% endblock %}
- {% block activity_stream %}
- {{ c.group_activity_stream | safe }}
- {% endblock %}
-
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 %} -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 %} +{{ 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 %} | +{% 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' %} +
{{ _('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 %} | -
- {{ 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 %} -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 %} ++ {{ 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 %} +Admin: Can add/edit and delete datasets, as well as - manage organization members.
-Editor: Can add and edit datasets, but not manage - organization members.
-Member: Can view the organization's private - datasets, but not add new datasets.
- {% endtrans %} -Admin: Can add/edit and delete datasets, as well as + manage organization members.
+Editor: Can add and edit datasets, but not manage + organization members.
+Member: Can view the organization's private + datasets, but not add new datasets.
+ {% endtrans %}{{ _('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?')}) %} - - | -
{{ _('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?')}) %} + + | +
Organizations act like publishing departments for datasets (for + example, the Department of Health). This means that datasets can be + published by and belong to a department instead of an individual + user.
+Within organizations, admins can assign roles and authorisation its + members, giving individual users the right to publish datasets from + that particular organisation (e.g. Office of National Statistics).
+ {% endtrans %} +Please try another search.
- {% endtrans %} - {% endif %} -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 %}