diff --git a/ckan/authz.py b/ckan/authz.py
index d77f91b887d..5fa6ad915ed 100644
--- a/ckan/authz.py
+++ b/ckan/authz.py
@@ -1,7 +1,6 @@
# encoding: utf-8
import sys
-import re
from logging import getLogger
from ckan.common import config
@@ -89,7 +88,7 @@ def _build(self):
self._functions.update(fetched_auth_functions)
_AuthFunctions = AuthFunctions()
-#remove the class
+# remove the class
del AuthFunctions
@@ -111,7 +110,8 @@ def is_sysadmin(username):
def _get_user(username):
- ''' Try to get the user from c, if possible, and fallback to using the DB '''
+ '''Try to get the user from c, if possible, and fallback to using the DB
+ '''
if not username:
return None
# See if we can get the user without touching the DB
@@ -148,11 +148,22 @@ def is_authorized(action, context, data_dict=None):
if context.get('ignore_auth'):
return {'success': True}
+ read_only = asbool(config.get('ckan.read_only', 'false'))
+
auth_function = _AuthFunctions.get(action)
if auth_function:
username = context.get('user')
- user = _get_user(username)
+ if read_only:
+ # If the auth function has not been wrapped with a auth_read_safe
+ # decorator, we should deny it if read-only mode is on.
+ if not getattr(auth_function, 'auth_read_safe', False):
+ return {
+ 'success': False,
+ 'msg': 'Read-only mode is enabled.'
+ }
+
+ user = _get_user(username)
if user:
# deleted users are always unauthorized
if user.is_deleted():
@@ -169,10 +180,12 @@ def is_authorized(action, context, data_dict=None):
# access straight away
if not getattr(auth_function, 'auth_allow_anonymous_access', False) \
and not context.get('auth_user_obj'):
- return {'success': False,
- 'msg': '{0} requires an authenticated user'
- .format(auth_function)
- }
+ return {
+ 'success': False,
+ 'msg': '{0} requires an authenticated user'.format(
+ auth_function
+ )
+ }
return auth_function(context, data_dict)
else:
@@ -182,7 +195,13 @@ def is_authorized(action, context, data_dict=None):
# these are the permissions that roles have
ROLE_PERMISSIONS = OrderedDict([
('admin', ['admin']),
- ('editor', ['read', 'delete_dataset', 'create_dataset', 'update_dataset', 'manage_group']),
+ ('editor', [
+ 'read',
+ 'delete_dataset',
+ 'create_dataset',
+ 'update_dataset',
+ 'manage_group'
+ ]),
('member', ['read', 'manage_group']),
])
@@ -253,7 +272,8 @@ def has_user_permission_for_group_or_org(group_id, user_name, permission):
return True
# Handle when permissions cascade. Check the user's roles on groups higher
# in the group hierarchy for permission.
- for capacity in check_config_permission('roles_that_cascade_to_sub_groups'):
+ for capacity in check_config_permission(
+ 'roles_that_cascade_to_sub_groups'):
parent_groups = group.get_parent_group_hierarchy(type=group.type)
group_ids = [group_.id for group_ in parent_groups]
if _has_user_permission_for_groups(user_id, permission, group_ids,
@@ -335,7 +355,7 @@ def has_user_permission_for_some_org(user_name, permission):
# see if any of the groups are orgs
q = model.Session.query(model.Group) \
- .filter(model.Group.is_organization == True) \
+ .filter(model.Group.is_organization.is_(True)) \
.filter(model.Group.state == 'active') \
.filter(model.Group.id.in_(group_ids))
@@ -417,6 +437,7 @@ def auth_is_registered_user():
'''
return auth_is_loggedin_user()
+
def auth_is_loggedin_user():
''' Do we have a logged in user '''
try:
@@ -425,6 +446,7 @@ def auth_is_loggedin_user():
context_user = None
return bool(context_user)
+
def auth_is_anon_user(context):
''' Is this an anonymous user?
eg Not logged in if a web request and not user defined in context
diff --git a/ckan/config/environment.py b/ckan/config/environment.py
index 14486378322..07a0870c0c4 100644
--- a/ckan/config/environment.py
+++ b/ckan/config/environment.py
@@ -2,11 +2,16 @@
'''CKAN environment configuration'''
import os
+import tempfile
+import atexit
+import shutil
+from functools import partial
import logging
import warnings
from urlparse import urlparse
import pytz
+import jinja2
import sqlalchemy
from pylons import config as pylons_config
import formencode
@@ -230,17 +235,32 @@ def update_config():
logging.getLogger("MARKDOWN").setLevel(logging.getLogger().level)
# Create Jinja2 environment
+ cache_dir = config.get('jinja2_cache_dir', None)
+ if not cache_dir:
+ cache_dir = tempfile.mkdtemp()
+ config['jinja2_cache_dir'] = cache_dir
+ atexit.register(partial(shutil.rmtree, cache_dir))
+ elif not os.path.exists(cache_dir):
+ os.makedirs(cache_dir)
env = jinja_extensions.Environment(
loader=jinja_extensions.CkanFileSystemLoader(template_paths),
autoescape=True,
- extensions=['jinja2.ext.do', 'jinja2.ext.with_',
- jinja_extensions.SnippetExtension,
- jinja_extensions.CkanExtend,
- jinja_extensions.CkanInternationalizationExtension,
- jinja_extensions.LinkForExtension,
- jinja_extensions.ResourceExtension,
- jinja_extensions.UrlForStaticExtension,
- jinja_extensions.UrlForExtension]
+ auto_reload=False,
+ extensions=[
+ 'jinja2.ext.do',
+ 'jinja2.ext.with_',
+ 'jinja2.ext.InternationalizationExtension',
+ jinja_extensions.SnippetExtension,
+ jinja_extensions.CkanExtend,
+ jinja_extensions.LinkForExtension,
+ jinja_extensions.ResourceExtension,
+ jinja_extensions.UrlForStaticExtension,
+ jinja_extensions.UrlForExtension
+ ],
+ # The pre-2.8 default was only 50, the post-2.8 default is 400.
+ cache_size=400,
+
+ bytecode_cache=jinja2.FileSystemBytecodeCache(cache_dir)
)
env.install_gettext_callables(_, ungettext, newstyle=True)
# custom filters
diff --git a/ckan/config/routing.py b/ckan/config/routing.py
index 03aa50f625b..b8e28f62b42 100644
--- a/ckan/config/routing.py
+++ b/ckan/config/routing.py
@@ -240,16 +240,16 @@ def make_map():
'api_data',
])))
m.connect('dataset_edit', '/dataset/edit/{id}', action='edit',
- ckan_icon='edit')
+ ckan_icon='pencil-square-o')
m.connect('dataset_followers', '/dataset/followers/{id}',
- action='followers', ckan_icon='group')
+ action='followers', ckan_icon='users')
m.connect('dataset_activity', '/dataset/activity/{id}',
- action='activity', ckan_icon='time')
+ action='activity', ckan_icon='clock-o')
m.connect('/dataset/activity/{id}/{offset}', action='activity')
m.connect('dataset_groups', '/dataset/groups/{id}',
- action='groups', ckan_icon='group')
+ action='groups', ckan_icon='users')
m.connect('dataset_resources', '/dataset/resources/{id}',
- action='resources', ckan_icon='reorder')
+ action='resources', ckan_icon='bars')
m.connect('dataset_read', '/dataset/{id}', action='read',
ckan_icon='sitemap')
m.connect('/dataset/{id}/resource/{resource_id}',
@@ -257,7 +257,7 @@ def make_map():
m.connect('/dataset/{id}/resource_delete/{resource_id}',
action='resource_delete')
m.connect('resource_edit', '/dataset/{id}/resource_edit/{resource_id}',
- action='resource_edit', ckan_icon='edit')
+ action='resource_edit', ckan_icon='pencil-square-o')
m.connect('/dataset/{id}/resource/{resource_id}/download',
action='resource_download')
m.connect('/dataset/{id}/resource/{resource_id}/download/{filename}',
@@ -270,12 +270,12 @@ def make_map():
m.connect('/dataset/{id}/resource/{resource_id}/preview',
action='resource_datapreview')
m.connect('views', '/dataset/{id}/resource/{resource_id}/views',
- action='resource_views', ckan_icon='reorder')
+ action='resource_views', ckan_icon='bars')
m.connect('new_view', '/dataset/{id}/resource/{resource_id}/new_view',
- action='edit_view', ckan_icon='edit')
+ action='edit_view', ckan_icon='pencil-square-o')
m.connect('edit_view',
'/dataset/{id}/resource/{resource_id}/edit_view/{view_id}',
- action='edit_view', ckan_icon='edit')
+ action='edit_view', ckan_icon='pencil-square-o')
m.connect('resource_view',
'/dataset/{id}/resource/{resource_id}/view/{view_id}',
action='resource_view')
@@ -307,13 +307,13 @@ def make_map():
'activity',
])))
m.connect('group_about', '/group/about/{id}', action='about',
- ckan_icon='info-sign'),
+ ckan_icon='info-circle'),
m.connect('group_edit', '/group/edit/{id}', action='edit',
- ckan_icon='edit')
+ ckan_icon='pencil-square-o')
m.connect('group_members', '/group/members/{id}', action='members',
- ckan_icon='group'),
+ ckan_icon='users'),
m.connect('group_activity', '/group/activity/{id}/{offset}',
- action='activity', ckan_icon='time'),
+ action='activity', ckan_icon='clock-o'),
m.connect('group_read', '/group/{id}', action='read',
ckan_icon='sitemap')
@@ -331,16 +331,16 @@ def make_map():
'history'
])))
m.connect('organization_activity', '/organization/activity/{id}/{offset}',
- action='activity', ckan_icon='time')
+ action='activity', ckan_icon='clock-o')
m.connect('organization_read', '/organization/{id}', action='read')
m.connect('organization_about', '/organization/about/{id}',
- action='about', ckan_icon='info-sign')
+ action='about', ckan_icon='info-circle')
m.connect('organization_read', '/organization/{id}', action='read',
ckan_icon='sitemap')
m.connect('organization_edit', '/organization/edit/{id}',
- action='edit', ckan_icon='edit')
+ action='edit', ckan_icon='pencil-square-o')
m.connect('organization_members', '/organization/members/{id}',
- action='members', ckan_icon='group')
+ action='members', ckan_icon='users')
m.connect('organization_bulk_process',
'/organization/bulk_process/{id}',
action='bulk_process', ckan_icon='sitemap')
@@ -364,20 +364,20 @@ def make_map():
m.connect('user_generate_apikey', '/user/generate_key/{id}', action='generate_apikey')
m.connect('/user/activity/{id}/{offset}', action='activity')
m.connect('user_activity_stream', '/user/activity/{id}',
- action='activity', ckan_icon='time')
+ action='activity', ckan_icon='clock-o')
m.connect('user_dashboard', '/dashboard', action='dashboard',
ckan_icon='list')
m.connect('user_dashboard_datasets', '/dashboard/datasets',
action='dashboard_datasets', ckan_icon='sitemap')
m.connect('user_dashboard_groups', '/dashboard/groups',
- action='dashboard_groups', ckan_icon='group')
+ action='dashboard_groups', ckan_icon='users')
m.connect('user_dashboard_organizations', '/dashboard/organizations',
- action='dashboard_organizations', ckan_icon='building')
+ action='dashboard_organizations', ckan_icon='building-o')
m.connect('/dashboard/{offset}', action='dashboard')
m.connect('user_follow', '/user/follow/{id}', action='follow')
m.connect('/user/unfollow/{id}', action='unfollow')
m.connect('user_followers', '/user/followers/{id:.*}',
- action='followers', ckan_icon='group')
+ action='followers', ckan_icon='users')
m.connect('user_edit', '/user/edit/{id:.*}', action='edit',
ckan_icon='cog')
m.connect('user_delete', '/user/delete/{id}', action='delete')
@@ -395,13 +395,6 @@ def make_map():
ckan_icon='sitemap')
m.connect('user_index', '/user', action='index')
- with SubMapper(map, controller='revision') as m:
- m.connect('/revision', action='index')
- m.connect('/revision/edit/{id}', action='edit')
- m.connect('/revision/diff/{id}', action='diff')
- m.connect('/revision/list', action='list')
- m.connect('/revision/{id}', action='read')
-
# feeds
with SubMapper(map, controller='feed') as m:
m.connect('/feeds/group/{id}.atom', action='group')
@@ -411,11 +404,11 @@ def make_map():
m.connect('/feeds/custom.atom', action='custom')
map.connect('ckanadmin_index', '/ckan-admin', controller='admin',
- action='index', ckan_icon='legal')
+ action='index', ckan_icon='gavel')
map.connect('ckanadmin_config', '/ckan-admin/config', controller='admin',
- action='config', ckan_icon='check')
+ action='config', ckan_icon='check-square-o')
map.connect('ckanadmin_trash', '/ckan-admin/trash', controller='admin',
- action='trash', ckan_icon='trash')
+ action='trash', ckan_icon='trash-o')
map.connect('ckanadmin', '/ckan-admin/{action}', controller='admin')
with SubMapper(map, controller='ckan.controllers.storage:StorageController') as m:
diff --git a/ckan/config/solr/schema.xml b/ckan/config/solr/schema.xml
index e8893f70ff9..409b6fa957f 100644
--- a/ckan/config/solr/schema.xml
+++ b/ckan/config/solr/schema.xml
@@ -118,6 +118,10 @@ schema. In this case the version should be set to the next CKAN version number.
+
+
+
+
diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py
index b0949e3bbeb..580e7a0d190 100644
--- a/ckan/controllers/group.py
+++ b/ckan/controllers/group.py
@@ -295,20 +295,45 @@ def pager_url(q=None, page=None):
try:
c.fields = []
- c.fields_grouped = {}
search_extras = {}
- for (param, value) in request.params.items():
- if not param in ['q', 'page', 'sort'] \
- and len(value) and not param.startswith('_'):
- if not param.startswith('ext_'):
- c.fields.append((param, value))
- q += ' %s: "%s"' % (param, value)
- if param not in c.fields_grouped:
- c.fields_grouped[param] = [value]
- else:
- c.fields_grouped[param].append(value)
+ c.fields_grouped = {}
+ fq_list = []
+ query_params = request.params.mixed()
+ for (param, value) in query_params.iteritems():
+ if param in ('q', 'page', 'sort') or not value:
+ continue
+ elif param.startswith('_'):
+ continue
+ elif param.startswith('ext_'):
+ search_extras[param] = value
+ else:
+ if isinstance(value, (list, tuple)):
+ c.fields.extend((param, v) for v in value)
+ # We're filtering on a list of items, each of which
+ # should be escaped and OR'd instead of Solr's default
+ # AND.
+ filter_value = u'({0})'.format(
+ u' OR '.join(u'"{0}"'.format(
+ v
+ ) for v in value)
+ )
else:
- search_extras[param] = value
+ c.fields.append((param, value))
+ # We're just filtering on a single item, which might be
+ # a range. We assume it's a range if it starts with a
+ # [, otherwise we escape it and treat it as a literal.
+ filter_value = (
+ value if value.startswith(u'[')
+ else u'"{0}"'.format(value)
+ )
+
+ # Tag each value with a domain so we can act on it later.
+ fq_list.append(u'{{!tag={p}}}{p}:{v}'.format(
+ p=param,
+ v=filter_value
+ ))
+
+ c.fields_grouped.setdefault(param, []).append(value)
include_private = False
user_member_of_orgs = [org['id'] for org
@@ -342,9 +367,15 @@ def pager_url(q=None, page=None):
data_dict = {
'q': q,
- 'fq': '',
+ 'fq': fq,
'include_private': include_private,
- 'facet.field': facets.keys(),
+ 'fq_list': fq_list,
+ 'facet.field': [
+ # When faceting, exclude the facet group from the facet
+ # counts. This lets us always get a count back, rather than
+ # an intersection (which would always be 0)
+ '{{!ex={k}}}{k}'.format(k=k) for k in facets.iterkeys()
+ ],
'rows': limit,
'sort': sort_by,
'start': (page - 1) * limit,
@@ -856,18 +887,23 @@ def activity(self, id, offset=0):
except (NotFound, NotAuthorized):
abort(404, _('Group not found'))
- try:
- # Add the group's activity stream (already rendered to HTML) to the
- # template context for the group/read.html
- # template to retrieve later.
- c.group_activity_stream = self._action('group_activity_list_html')(
- context, {'id': c.group_dict['id'], 'offset': offset})
-
- except ValidationError as error:
- base.abort(400)
-
- return render(self._activity_template(group_type),
- extra_vars={'group_type': group_type})
+ activity_action = 'group_activity_list'
+ if 'organization' in self.group_types:
+ activity_action = 'organization_activity_list'
+
+ return render(
+ self._activity_template(group_type),
+ extra_vars={
+ 'group_type': group_type,
+ 'activity_stream': get_action(activity_action)(
+ context,
+ {
+ 'id': c.group_dict['id'],
+ 'offset': offset
+ }
+ )
+ }
+ )
def follow(self, id):
'''Start following this group.'''
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index ee3d5b08ae2..7f267306434 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -1,8 +1,8 @@
# encoding: utf-8
+import time
import logging
from urllib import urlencode
-import datetime
import mimetypes
import cgi
@@ -13,7 +13,6 @@
import ckan.logic as logic
import ckan.lib.base as base
import ckan.lib.maintain as maintain
-import ckan.lib.i18n as i18n
import ckan.lib.navl.dictization_functions as dict_fns
import ckan.lib.helpers as h
import ckan.model as model
@@ -57,7 +56,7 @@ def url_with_params(url, params):
def search_url(params, package_type=None):
if not package_type or package_type == 'dataset':
- url = h.url_for(controller='package', action='search')
+ url = h.url_for(controller=c.controller, action='search')
else:
url = h.url_for('{0}_search'.format(package_type))
return url_with_params(url, params)
@@ -207,33 +206,68 @@ def pager_url(q=None, page=None):
# a list of values eg {'tags':['tag1', 'tag2']}
c.fields_grouped = {}
search_extras = {}
- fq = ''
- for (param, value) in request.params.items():
- if param not in ['q', 'page', 'sort'] \
- and len(value) and not param.startswith('_'):
- if not param.startswith('ext_'):
- c.fields.append((param, value))
- fq += ' %s:"%s"' % (param, value)
- if param not in c.fields_grouped:
- c.fields_grouped[param] = [value]
- else:
- c.fields_grouped[param].append(value)
+ fq = []
+ fq_list = []
+ # FIXME: This seems moderately insane - it treats *every* argument
+ # to the URL as a fq filter. We should have more knowledge of what
+ # we expect to see passed in, no need to guess.
+ query_params = request.params.mixed()
+ for param, value in query_params.iteritems():
+ if param in ('q', 'page', 'sort') or not value:
+ continue
+ elif param.startswith('_'):
+ continue
+ elif param.startswith('ext_'):
+ search_extras[param] = value
+ else:
+ if isinstance(value, (list, tuple)):
+ c.fields.extend((param, v) for v in value)
+ # We're filtering on a list of items, each of which
+ # should be escaped and OR'd instead of Solr's default
+ # AND.
+ filter_value = u'({0})'.format(
+ u' OR '.join(u'"{0}"'.format(
+ v
+ ) for v in value)
+ )
else:
- search_extras[param] = value
-
- context = {'model': model, 'session': model.Session,
- 'user': c.user, 'for_view': True,
- 'auth_user_obj': c.userobj}
+ c.fields.append((param, value))
+ # We're just filtering on a single item, which might be
+ # a range. We assume it's a range if it starts with a
+ # [, otherwise we escape it and treat it as a literal.
+ filter_value = (
+ value if value.startswith(u'[')
+ else u'"{0}"'.format(value)
+ )
+
+ # Tag each value with a domain so we can act on it later.
+ fq_list.append(u'{{!tag={p}}}{p}:{v}'.format(
+ p=param,
+ v=filter_value
+ ))
+
+ c.fields_grouped.setdefault(param, []).append(value)
+
+ context = {
+ 'model': model,
+ 'session': model.Session,
+ 'user': c.user,
+ 'for_view': True,
+ 'auth_user_obj': c.userobj
+ }
if package_type and package_type != 'dataset':
# Only show datasets of this particular type
- fq += ' +dataset_type:{type}'.format(type=package_type)
+ fq.append('+dataset_type:{type}'.format(type=package_type))
else:
# Unless changed via config options, don't show non standard
# dataset types on the default search page
- if not asbool(
- config.get('ckan.search.show_all_types', 'False')):
- fq += ' +dataset_type:dataset'
+ show_all_types = config.get(
+ 'ckan.search.show_all_types',
+ 'False'
+ )
+ if not asbool(show_all_types):
+ fq.append('+dataset_type:dataset')
facets = OrderedDict()
@@ -243,7 +277,7 @@ def pager_url(q=None, page=None):
'tags': _('Tags'),
'res_format': _('Formats'),
'license_id': _('Licenses'),
- }
+ }
for facet in g.facets:
if facet in default_facet_titles:
@@ -259,8 +293,14 @@ def pager_url(q=None, page=None):
data_dict = {
'q': q,
- 'fq': fq.strip(),
- 'facet.field': facets.keys(),
+ 'fq': ' '.join(fq),
+ 'fq_list': fq_list,
+ 'facet.field': [
+ # When faceting, exclude the facet group from the facet
+ # counts. This lets us always get a count back, rather than
+ # an intersection (which would always be 0)
+ '{{!ex={k}}}{k}'.format(k=k) for k in facets.iterkeys()
+ ],
'rows': limit,
'start': (page - 1) * limit,
'sort': sort_by,
@@ -281,6 +321,7 @@ def pager_url(q=None, page=None):
)
c.facets = query['facets']
c.search_facets = query['search_facets']
+ c.facet_ranges = query['facet_ranges']
c.page.items = query['results']
except SearchQueryError, se:
# User's search parameters are invalid, in such a way that is not
@@ -350,31 +391,38 @@ def read(self, id):
'user': c.user, 'for_view': True,
'auth_user_obj': c.userobj}
data_dict = {'id': id, 'include_tracking': True}
-
- # interpret @ or @ suffix
- split = id.split('@')
- if len(split) == 2:
- data_dict['id'], revision_ref = split
- if model.is_id(revision_ref):
- context['revision_id'] = revision_ref
- else:
- try:
- date = h.date_str_to_datetime(revision_ref)
- context['revision_date'] = date
- except TypeError, e:
- abort(400, _('Invalid revision format: %r') % e.args)
- except ValueError, e:
- abort(400, _('Invalid revision format: %r') % e.args)
- elif len(split) > 2:
- abort(400, _('Invalid revision format: %r') %
- 'Too many "@" symbols')
+ activity_id = request.params.get('activity_id')
# check if package exists
try:
c.pkg_dict = get_action('package_show')(context, data_dict)
c.pkg = context['package']
- except (NotFound, NotAuthorized):
+
+ if activity_id:
+ c.pkg_dict = context['session'].query(model.Activity).get(
+ activity_id
+ ).data['package']
+ # Don't crash on old activity records, which do not include
+ # resources or extras.
+ c.pkg_dict.setdefault('resources', [])
+ c.is_activity_archive = True
+ except NotFound:
abort(404, _('Dataset not found'))
+ except NotAuthorized:
+ tmp_context = context.copy()
+ tmp_context['ignore_auth'] = True
+
+ pkg = get_action('package_show')(tmp_context, data_dict)
+ if pkg['state'] == 'deleted':
+ # We're not authorized because this dataset has been deleted
+ # and we're not allowed to see deleted packages. Instead of
+ # 404ing as in core CKAN we display a "Dataset Deleted" page.
+ return render('package/deleted.html', extra_vars={
+ 'created': pkg['metadata_created'],
+ 'modified': pkg['metadata_modified'],
+ 'organization': pkg.get('organization', {}).get('title'),
+ 'site_url': config.get('ckan.site_url')
+ })
# used by disqus plugin
c.current_package_id = c.pkg.id
@@ -395,8 +443,12 @@ def read(self, id):
template = self._read_template(package_type)
try:
- return render(template,
- extra_vars={'dataset_type': package_type})
+ return render(
+ template,
+ extra_vars={
+ 'dataset_type': package_type
+ }
+ )
except ckan.lib.render.TemplateNotFound:
msg = _("Viewing {package_type} datasets in {format} format is "
"not supported (template file {file} not found).".format(
@@ -407,87 +459,7 @@ def read(self, id):
assert False, "We should never get here"
def history(self, id):
-
- if 'diff' in request.params or 'selected1' in request.params:
- try:
- params = {'id': request.params.getone('pkg_name'),
- 'diff': request.params.getone('selected1'),
- 'oldid': request.params.getone('selected2'),
- }
- except KeyError:
- if 'pkg_name' in dict(request.params):
- id = request.params.getone('pkg_name')
- c.error = \
- _('Select two revisions before doing the comparison.')
- else:
- params['diff_entity'] = 'package'
- h.redirect_to(controller='revision', action='diff', **params)
-
- context = {'model': model, 'session': model.Session,
- 'user': c.user, 'auth_user_obj': c.userobj,
- 'for_view': True}
- data_dict = {'id': id}
- try:
- c.pkg_dict = get_action('package_show')(context, data_dict)
- c.pkg_revisions = get_action('package_revision_list')(context,
- data_dict)
- # TODO: remove
- # Still necessary for the authz check in group/layout.html
- c.pkg = context['package']
-
- except NotAuthorized:
- abort(403, _('Unauthorized to read package %s') % '')
- except NotFound:
- abort(404, _('Dataset not found'))
-
- format = request.params.get('format', '')
- if format == 'atom':
- # Generate and return Atom 1.0 document.
- from webhelpers.feedgenerator import Atom1Feed
- feed = Atom1Feed(
- title=_(u'CKAN Dataset Revision History'),
- link=h.url_for(controller='revision', action='read',
- id=c.pkg_dict['name']),
- description=_(u'Recent changes to CKAN Dataset: ') +
- (c.pkg_dict['title'] or ''),
- language=unicode(i18n.get_lang()),
- )
- for revision_dict in c.pkg_revisions:
- revision_date = h.date_str_to_datetime(
- revision_dict['timestamp'])
- try:
- dayHorizon = int(request.params.get('days'))
- except:
- dayHorizon = 30
- dayAge = (datetime.datetime.now() - revision_date).days
- if dayAge >= dayHorizon:
- break
- if revision_dict['message']:
- item_title = u'%s' % revision_dict['message'].\
- split('\n')[0]
- else:
- item_title = u'%s' % revision_dict['id']
- item_link = h.url_for(controller='revision', action='read',
- id=revision_dict['id'])
- item_description = _('Log message: ')
- item_description += '%s' % (revision_dict['message'] or '')
- item_author_name = revision_dict['author']
- item_pubdate = revision_date
- feed.add_item(
- title=item_title,
- link=item_link,
- description=item_description,
- author_name=item_author_name,
- pubdate=item_pubdate,
- )
- response.headers['Content-Type'] = 'application/atom+xml'
- return feed.writeString('utf-8')
-
- package_type = c.pkg_dict['type'] or 'dataset'
-
- return render(
- self._history_template(c.pkg_dict.get('type', package_type)),
- extra_vars={'dataset_type': package_type})
+ h.redirect_to(controller='package', action='activity', id=id)
def new(self, data=None, errors=None, error_summary=None):
if data and 'type' in data:
@@ -635,8 +607,8 @@ def new_resource(self, id, data=None, errors=None, error_summary=None):
# see if we have any data that we are trying to save
data_provided = False
for key, value in data.iteritems():
- if ((value or isinstance(value, cgi.FieldStorage))
- and key != 'resource_type'):
+ if ((value or isinstance(value, cgi.FieldStorage)) and
+ key != 'resource_type'):
data_provided = True
break
@@ -681,6 +653,10 @@ def new_resource(self, id, data=None, errors=None, error_summary=None):
get_action('resource_update')(context, data)
else:
get_action('resource_create')(context, data)
+ if not data['resource_type']:
+ h.flash_success(_('A related item has been added'))
+ else:
+ h.flash_success(_('A resource has been added'))
except ValidationError, e:
errors = e.error_dict
error_summary = e.error_summary
@@ -706,6 +682,9 @@ def new_resource(self, id, data=None, errors=None, error_summary=None):
# go to first stage of add dataset
redirect(h.url_for(controller='package',
action='read', id=id))
+ elif save_action == 'go-dataset-search':
+ redirect(h.url_for(controller='package',
+ action='search'))
else:
# add more resources
redirect(h.url_for(controller='package',
@@ -1161,7 +1140,7 @@ def resource_download(self, id, resource_id, filename=None):
response.headers['Content-Type'] = content_type
response.status = status
return app_iter
- elif not 'url' in rsc:
+ elif 'url' not in rsc:
abort(404, _('No download is available'))
redirect(rsc['url'])
@@ -1177,8 +1156,7 @@ def follow(self, id):
h.flash_success(_("You are now following {0}").format(
package_dict['title']))
except ValidationError as e:
- error_message = (e.message or e.error_summary
- or e.error_dict)
+ error_message = (e.message or e.error_summary or e.error_dict)
h.flash_error(error_message)
except NotAuthorized as e:
h.flash_error(e.message)
@@ -1196,8 +1174,7 @@ def unfollow(self, id):
h.flash_success(_("You are no longer following {0}").format(
package_dict['title']))
except ValidationError as e:
- error_message = (e.message or e.error_summary
- or e.error_dict)
+ error_message = (e.message or e.error_summary or e.error_dict)
h.flash_error(error_message)
except (NotFound, NotAuthorized) as e:
error_message = e.message
@@ -1283,27 +1260,47 @@ def groups(self, id):
return render('package/group_list.html',
{'dataset_type': dataset_type})
- def activity(self, id):
+ def activity(self, id, offset=None):
'''Render this package's public activity stream page.'''
- context = {'model': model, 'session': model.Session,
- 'user': c.user, 'for_view': True,
- 'auth_user_obj': c.userobj}
- data_dict = {'id': id}
+ context = {
+ 'model': model,
+ 'session': model.Session,
+ 'user': c.user,
+ 'for_view': True,
+ 'auth_user_obj': c.userobj
+ }
+
try:
- c.pkg_dict = get_action('package_show')(context, data_dict)
- c.pkg = context['package']
- c.package_activity_stream = get_action(
- 'package_activity_list_html')(
- context, {'id': c.pkg_dict['id']})
+ c.pkg_dict = get_action('package_show')(context, {
+ 'id': id
+ })
dataset_type = c.pkg_dict['type'] or 'dataset'
except NotFound:
abort(404, _('Dataset not found'))
except NotAuthorized:
abort(403, _('Unauthorized to read dataset %s') % id)
- return render('package/activity.html',
- {'dataset_type': dataset_type})
+ # FIXME: Temporary patch until activity refactor.
+ limit = int(config.get('ckan.activity_list_limit', 31))
+
+ return render(
+ 'package/activity.html',
+ extra_vars={
+ 'dataset_type': dataset_type,
+ 'activity_stream': get_action('package_activity_list')(
+ context,
+ {
+ 'id': id,
+ 'offset': offset,
+ 'limit': limit
+ }
+ ),
+ 'limit': limit,
+ 'ts': lambda t: int(time.mktime(t.timetuple())),
+ 'offset': offset
+ }
+ )
def resource_embedded_dataviewer(self, id, resource_id,
width=500, height=500):
@@ -1465,7 +1462,7 @@ def edit_view(self, id, resource_id, view_id=None):
else:
data = get_action('resource_view_create')(context, data)
except ValidationError, e:
- ## Could break preview if validation error
+ # Could break preview if validation error
to_preview = False
errors = e.error_dict
error_summary = e.error_summary
@@ -1479,14 +1476,14 @@ def edit_view(self, id, resource_id, view_id=None):
action='resource_views',
id=id, resource_id=resource_id))
- ## view_id exists only when updating
+ # view_id exists only when updating
if view_id:
try:
old_data = get_action('resource_view_show')(context,
{'id': view_id})
data = data or old_data
view_type = old_data.get('view_type')
- ## might as well preview when loading good existing view
+ # might as well preview when loading good existing view
if not errors:
to_preview = True
except (NotFound, NotAuthorized):
diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py
index ebc305d19f0..1a84d7dcabf 100644
--- a/ckan/controllers/user.py
+++ b/ckan/controllers/user.py
@@ -1,7 +1,6 @@
# encoding: utf-8
import logging
-from urllib import quote
from ckan.common import config
from paste.deploy.converters import asbool
@@ -18,7 +17,7 @@
import ckan.lib.authenticator as authenticator
import ckan.plugins as p
-from ckan.common import _, c, g, request, response
+from ckan.common import _, c, request, response
log = logging.getLogger(__name__)
@@ -37,6 +36,10 @@
unflatten = dictization_functions.unflatten
+def require_sudo_mode():
+ pass
+
+
def set_repoze_user(user_id):
'''Set the repoze.who cookie to match a given user_id'''
if 'repoze.who.plugins' in request.environ:
@@ -57,7 +60,6 @@ def __before__(self, action, **env):
if c.action not in ('login', 'request_reset', 'perform_reset',):
abort(403, _('Not authorized to see this page'))
- ## hooks for subclasses
new_user_form = 'user/new_user_form.html'
edit_user_form = 'user/edit_user_form.html'
@@ -88,8 +90,6 @@ def _setup_template_variables(self, context, data_dict):
c.is_myself = user_dict['name'] == c.user
c.about_formatted = h.render_markdown(user_dict['about'])
- ## end hooks
-
def _get_repoze_handler(self, handler_name):
'''Returns the URL that repoze.who will respond to and perform a
login or logout.'''
@@ -136,20 +136,13 @@ def read(self, id=None):
'include_num_followers': True}
self._setup_template_variables(context, data_dict)
-
- # The legacy templates have the user's activity stream on the user
- # profile page, new templates do not.
- if asbool(config.get('ckan.legacy_templates', False)):
- c.user_activity_stream = get_action('user_activity_list_html')(
- context, {'id': c.user_dict['id']})
-
return render('user/read.html')
def me(self, locale=None):
if not c.user:
h.redirect_to(locale=locale, controller='user', action='login',
id=None)
- user_ref = c.userobj.get_reference_preferred_for_uri()
+
h.redirect_to(locale=locale, controller='user', action='dashboard')
def register(self, data=None, errors=None, error_summary=None):
@@ -240,7 +233,7 @@ def _save_new(self, context):
logic.tuplize_dict(logic.parse_params(request.params))))
context['message'] = data_dict.get('log_message', '')
captcha.check_recaptcha(request)
- user = get_action('user_create')(context, data_dict)
+ get_action('user_create')(context, data_dict)
except NotAuthorized:
abort(403, _('Unauthorized to create user %s') % '')
except NotFound, e:
@@ -275,16 +268,21 @@ def _save_new(self, context):
return render('user/logout_first.html')
def edit(self, id=None, data=None, errors=None, error_summary=None):
- context = {'save': 'save' in request.params,
- 'schema': self._edit_form_to_db_schema(),
- 'model': model, 'session': model.Session,
- 'user': c.user, 'auth_user_obj': c.userobj
- }
+ context = {
+ 'save': 'save' in request.params,
+ 'schema': self._edit_form_to_db_schema(),
+ 'model': model,
+ 'session': model.Session,
+ 'user': c.user,
+ 'auth_user_obj': c.userobj
+ }
+
if id is None:
if c.userobj:
id = c.userobj.id
else:
abort(400, _('No user specified'))
+
data_dict = {'id': id}
try:
@@ -292,6 +290,8 @@ def edit(self, id=None, data=None, errors=None, error_summary=None):
except NotAuthorized:
abort(403, _('Unauthorized to edit a user.'))
+ require_sudo_mode()
+
if (context['save']) and not data:
return self._save_edit(id, context)
@@ -321,7 +321,12 @@ def edit(self, id=None, data=None, errors=None, error_summary=None):
(str(c.user), id))
errors = errors or {}
- vars = {'data': data, 'errors': errors, 'error_summary': error_summary}
+ vars = {
+ 'data': data,
+ 'errors': errors,
+ 'error_summary': error_summary,
+ 'is_sysadmin': authz.is_sysadmin(c.user)
+ }
self._setup_template_variables({'model': model,
'session': model.Session,
@@ -408,7 +413,7 @@ def login(self, error=None):
vars = {}
return render('user/login.html', extra_vars=vars)
else:
- return render('user/logout_first.html')
+ return h.redirect_to(controller='user', action='logged_in')
def logged_in(self):
# redirect if needed
@@ -420,7 +425,7 @@ def logged_in(self):
context = None
data_dict = {'id': c.user}
- user_dict = get_action('user_show')(context, data_dict)
+ get_action('user_show')(context, data_dict)
return self.me()
else:
@@ -469,7 +474,7 @@ def request_reset(self):
data_dict = {'id': id}
user_obj = None
try:
- user_dict = get_action('user_show')(context, data_dict)
+ get_action('user_show')(context, data_dict)
user_obj = context['user_obj']
except NotFound:
# Try searching the user
@@ -484,7 +489,7 @@ def request_reset(self):
# and user_list does not return them
del data_dict['q']
data_dict['id'] = user_list[0]['id']
- user_dict = get_action('user_show')(context, data_dict)
+ get_action('user_show')(context, data_dict)
user_obj = context['user_obj']
elif len(user_list) > 1:
h.flash_error(_('"%s" matched several users') % (id))
@@ -531,11 +536,15 @@ def perform_reset(self, id):
if request.method == 'POST':
try:
context['reset_password'] = True
+ user_state = user_dict['state']
new_password = self._get_form_password()
- user_dict['password'] = new_password
+ username = request.params.get('name')
+ if (username is not None and username != ''):
+ user_dict['name'] = username
user_dict['reset_key'] = c.reset_key
user_dict['state'] = model.State.ACTIVE
- user = get_action('user_update')(context, user_dict)
+ user_dict['password'] = new_password
+ get_action('user_update')(context, user_dict)
mailer.create_reset_key(user_obj)
h.flash_success(_("Your password has been reset."))
@@ -550,6 +559,7 @@ def perform_reset(self, id):
h.flash_error(u'%r' % e.error_dict)
except ValueError, ve:
h.flash_error(unicode(ve))
+ user_dict['state'] = user_state
c.user_dict = user_dict
return render('user/perform_reset.html')
@@ -595,13 +605,18 @@ def activity(self, id, offset=0):
self._setup_template_variables(context, data_dict)
- try:
- c.user_activity_stream = get_action('user_activity_list_html')(
- context, {'id': c.user_dict['id'], 'offset': offset})
- except ValidationError:
- base.abort(400)
-
- return render('user/activity_stream.html')
+ return render(
+ 'user/activity_stream.html',
+ extra_vars={
+ 'activity_stream': get_action('user_activity_list')(
+ context,
+ {
+ 'id': id,
+ 'offset': offset
+ }
+ )
+ }
+ )
def _get_dashboard_context(self, filter_type=None, filter_id=None, q=None):
'''Return a dict needed by the dashboard view to determine context.'''
@@ -668,10 +683,19 @@ def dashboard(self, id=None, offset=0):
filter_id = request.params.get('name', u'')
c.followee_list = get_action('followee_list')(
- context, {'id': c.userobj.id, 'q': q})
+ context,
+ {
+ 'id': c.userobj.id,
+ 'q': q
+ }
+ )
+
c.dashboard_activity_stream_context = self._get_dashboard_context(
- filter_type, filter_id, q)
- c.dashboard_activity_stream = h.dashboard_activity_stream(
+ filter_type,
+ filter_id,
+ q
+ )
+ dashboard_activity_stream = h.dashboard_activity_stream(
c.userobj.id, filter_type, filter_id, offset
)
@@ -679,7 +703,9 @@ def dashboard(self, id=None, offset=0):
# dashboard page.
get_action('dashboard_mark_activities_old')(context, {})
- return render('user/dashboard.html')
+ return render('user/dashboard.html', extra_vars={
+ 'activity_stream': dashboard_activity_stream
+ })
def dashboard_datasets(self):
context = {'for_view': True, 'user': c.user,
@@ -715,8 +741,7 @@ def follow(self, id):
h.flash_success(_("You are now following {0}").format(
user_dict['display_name']))
except ValidationError as e:
- error_message = (e.message or e.error_summary
- or e.error_dict)
+ error_message = (e.message or e.error_summary or e.error_dict)
h.flash_error(error_message)
except NotAuthorized as e:
h.flash_error(e.message)
@@ -738,7 +763,6 @@ def unfollow(self, id):
error_message = e.message
h.flash_error(error_message)
except ValidationError as e:
- error_message = (e.error_summary or e.message
- or e.error_dict)
+ error_message = (e.error_summary or e.message or e.error_dict)
h.flash_error(error_message)
h.redirect_to(controller='user', action='read', id=id)
diff --git a/ckan/lib/activity_streams.py b/ckan/lib/activity_streams.py
index e5807b2cc3e..103d016d008 100644
--- a/ckan/lib/activity_streams.py
+++ b/ckan/lib/activity_streams.py
@@ -165,18 +165,18 @@ def activity_stream_string_follow_group(context, activity):
# A dictionary mapping activity types to the icons associated to them
activity_stream_string_icons = {
'added tag': 'tag',
- 'changed group': 'group',
+ 'changed group': 'users',
'changed package': 'sitemap',
- 'changed package_extra': 'edit',
+ 'changed package_extra': 'pencil-square-o',
'changed resource': 'file',
'changed user': 'user',
- 'deleted group': 'group',
+ 'deleted group': 'users',
'deleted package': 'sitemap',
- 'deleted package_extra': 'edit',
+ 'deleted package_extra': 'pencil-square-o',
'deleted resource': 'file',
- 'new group': 'group',
+ 'new group': 'users',
'new package': 'sitemap',
- 'new package_extra': 'edit',
+ 'new package_extra': 'pencil-square-o',
'new resource': 'file',
'new user': 'user',
'removed tag': 'tag',
diff --git a/ckan/lib/base.py b/ckan/lib/base.py
index 3334a662599..dede0a2b00e 100644
--- a/ckan/lib/base.py
+++ b/ckan/lib/base.py
@@ -94,90 +94,15 @@ def render(template_name, extra_vars=None, cache_key=None, cache_type=None,
.. todo::
Document the parameters of :py:func:`ckan.plugins.toolkit.render`.
-
'''
- def render_template():
- globs = extra_vars or {}
- globs.update(pylons_globals())
- # Using pylons.url() directly destroys the localisation stuff so
- # we remove it so any bad templates crash and burn
- del globs['url']
+ extras = extra_vars or {}
+ extras.update(pylons_globals())
- try:
- template_path, template_type = render_.template_info(template_name)
- except render_.TemplateNotFound:
- raise
-
- log.debug('rendering %s [%s]' % (template_path, template_type))
- if config.get('debug'):
- context_vars = globs.get('c')
- if context_vars:
- context_vars = dir(context_vars)
- debug_info = {'template_name': template_name,
- 'template_path': template_path,
- 'template_type': template_type,
- 'vars': globs,
- 'c_vars': context_vars,
- 'renderer': renderer}
- if 'CKAN_DEBUG_INFO' not in request.environ:
- request.environ['CKAN_DEBUG_INFO'] = []
- request.environ['CKAN_DEBUG_INFO'].append(debug_info)
-
- del globs['config']
- return render_jinja2(template_name, globs)
-
- if 'Pragma' in response.headers:
- del response.headers["Pragma"]
-
- ## Caching Logic
- allow_cache = True
- # Force cache or not if explicit.
- if cache_force is not None:
- allow_cache = cache_force
- # Do not allow caching of pages for logged in users/flash messages etc.
- elif session.last_accessed:
- allow_cache = False
- # Tests etc.
- elif 'REMOTE_USER' in request.environ:
- allow_cache = False
- # Don't cache if based on a non-cachable template used in this.
- elif request.environ.get('__no_cache__'):
- allow_cache = False
- # Don't cache if we have set the __no_cache__ param in the query string.
- elif request.params.get('__no_cache__'):
- allow_cache = False
- # Don't cache if we have extra vars containing data.
- elif extra_vars:
- for k, v in extra_vars.iteritems():
- allow_cache = False
- break
- # Record cachability for the page cache if enabled
- request.environ['CKAN_PAGE_CACHABLE'] = allow_cache
-
- if allow_cache:
- response.headers["Cache-Control"] = "public"
- try:
- cache_expire = int(config.get('ckan.cache_expires', 0))
- response.headers["Cache-Control"] += \
- ", max-age=%s, must-revalidate" % cache_expire
- except ValueError:
- pass
- else:
- # We do not want caching.
- response.headers["Cache-Control"] = "private"
- # Prevent any further rendering from being cached.
- request.environ['__no_cache__'] = True
-
- # Render Time :)
- try:
- return cached_template(template_name, render_template)
- except ckan.exceptions.CkanUrlException, e:
- raise ckan.exceptions.CkanUrlException(
- '\nAn Exception has been raised for template %s\n%s' %
- (template_name, e.message))
- except render_.TemplateNotFound:
- raise
+ del extras['url']
+ del extras['config']
+
+ return template.render(**extras)
class ValidationException(Exception):
diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py
index f3914cbd3ff..f2a8628c201 100644
--- a/ckan/lib/dictization/model_dictize.py
+++ b/ckan/lib/dictization/model_dictize.py
@@ -558,11 +558,10 @@ def user_dictize(user, context, include_password_hash=False):
result_dict['display_name'] = user.display_name
result_dict['email_hash'] = user.email_hash
- result_dict['number_of_edits'] = user.number_of_edits()
- result_dict['number_created_packages'] = user.number_created_packages(
- include_private_and_draft=context.get(
- 'count_private_and_draft_datasets', False))
-
+ # FIXME: Extremely poorly performing queries, we've emergency hardcoded
+ # these to 0 as they're completely unused.
+ result_dict['number_of_edits'] = 0
+ result_dict['number_created_packages'] = 0
requester = context.get('user')
reset_key = result_dict.pop('reset_key', None)
diff --git a/ckan/lib/dictization/model_save.py b/ckan/lib/dictization/model_save.py
index 60476666682..2206305d6ba 100644
--- a/ckan/lib/dictization/model_save.py
+++ b/ckan/lib/dictization/model_save.py
@@ -98,7 +98,18 @@ def package_extras_save(extra_dicts, obj, context):
session = context["session"]
extras_list = obj.extras_list
- old_extras = dict((extra.key, extra) for extra in extras_list)
+
+ # XXX: clean up duplicate keys
+ # we've created a mess with our portal_release_date field
+ old_extras = {}
+ for extra in extras_list:
+ if extra.key in old_extras:
+ if extra.state == 'deleted':
+ continue
+ state = 'pending-deleted' if context.get('pending') else 'deleted'
+ extra.state = state
+ continue
+ old_extras[extra.key] = extra
new_extras = {}
for extra_dict in extra_dicts or []:
diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py
index 21ce5c1c360..157bd8e800b 100644
--- a/ckan/lib/helpers.py
+++ b/ckan/lib/helpers.py
@@ -44,6 +44,7 @@
import ckan
from ckan.common import _, ungettext, g, c, request, session, json
+from ckan.plugins.core import plugin_loaded
from markupsafe import Markup, escape
log = logging.getLogger(__name__)
@@ -162,6 +163,29 @@ def url(*args, **kw):
return url_for(*args, **kw)
+@core_helper
+def get_site_protocol_and_host():
+ '''Return the protocol and host of the configured `ckan.site_url`.
+ This is needed to generate valid, full-qualified URLs.
+
+ If `ckan.site_url` is set like this::
+
+ ckan.site_url = http://example.com
+
+ Then this function would return a tuple `('http', 'example.com')`
+ If the setting is missing, `(None, None)` is returned instead.
+
+ '''
+ site_url = config.get('ckan.site_url', None)
+ if site_url is not None:
+ parsed_url = urlparse.urlparse(site_url)
+ return (
+ parsed_url.scheme.encode('utf-8'),
+ parsed_url.netloc.encode('utf-8')
+ )
+ return (None, None)
+
+
@core_helper
def get_site_protocol_and_host():
'''Return the protocol and host of the configured `ckan.site_url`.
@@ -368,6 +392,9 @@ def full_current_url():
for sharing etc '''
return (url_for(request.environ['CKAN_CURRENT_URL'], qualified=True))
+def current_url():
+ ''' Returns current url unquoted'''
+ return urllib.unquote(request.environ['CKAN_CURRENT_URL'])
@core_helper
def current_url():
@@ -533,7 +560,7 @@ def _create_link_text(text, **kwargs):
if kwargs.pop('inner_span', None):
text = literal('') + text + literal('')
if icon:
- text = literal(' ' % icon) + text
+ text = literal(' ' % icon) + text
return text
icon = kwargs.pop('icon', None)
@@ -1600,6 +1627,16 @@ def remove_url_param(key, value=None, replace=None, controller=None,
return _create_url_with_params(params=params, controller=controller,
action=action, extras=extras)
+def canonical_search_url():
+ ''' Return a url with all parameters removed except for the pagination parameter
+ This is useful for creating canonical urls for search pages, so that search engines do not
+ index many multiples of different search pages due to other search and faceting parameters
+ '''
+ try:
+ page_param = [(k, v) for k, v in request.params.items() if k == 'page']
+ return _search_url(page_param)
+ except ckan.exceptions.CkanUrlException:
+ return _search_url(None)
@core_helper
def include_resource(resource):
@@ -1728,15 +1765,15 @@ def dashboard_activity_stream(user_id, filter_type=None, filter_id=None,
if filter_type:
action_functions = {
- 'dataset': 'package_activity_list_html',
- 'user': 'user_activity_list_html',
- 'group': 'group_activity_list_html',
- 'organization': 'organization_activity_list_html',
+ 'dataset': 'package_activity_list',
+ 'user': 'user_activity_list',
+ 'group': 'group_activity_list',
+ 'organization': 'organization_activity_list',
}
action_function = logic.get_action(action_functions.get(filter_type))
return action_function(context, {'id': filter_id, 'offset': offset})
else:
- return logic.get_action('dashboard_activity_list_html')(
+ return logic.get_action('dashboard_activity_list')(
context, {'offset': offset})
@@ -2299,10 +2336,10 @@ def license_options(existing_license_id=None):
def get_translated(data_dict, field):
language = i18n.get_lang()
try:
- return data_dict[field + '_translated'][language]
+ return data_dict[field+'_translated'][language]
except KeyError:
- return data_dict.get(field, '')
-
+ val = data_dict.get(field, '')
+ return _(val) if val and isinstance(val, basestring) else val
@core_helper
def mail_to(email_address, name):
@@ -2340,6 +2377,7 @@ def radio(selected, id, checked):
# Useful additions from the stdlib.
core_helper(urlencode)
core_helper(clean_html, name='clean_html')
+core_helper(plugin_loaded)
def load_plugin_helpers():
diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py
index 6d89f67d6c9..ed91a37f29e 100644
--- a/ckan/lib/i18n.py
+++ b/ckan/lib/i18n.py
@@ -184,8 +184,8 @@ def handle_request(request, tmpl_context):
''' Set the language for the request '''
lang = request.environ.get('CKAN_LANG') or \
config.get('ckan.locale_default', 'en')
- if lang != 'en':
- set_lang(lang)
+
+ set_lang(lang)
for plugin in PluginImplementations(ITranslation):
if lang in plugin.i18n_locales():
@@ -234,5 +234,4 @@ def set_lang(language_code):
''' Wrapper to pylons call '''
if language_code in non_translated_locals():
language_code = config.get('ckan.locale_default', 'en')
- if language_code != 'en':
- _set_lang(language_code)
+ _set_lang(language_code)
diff --git a/ckan/lib/navl/validators.py b/ckan/lib/navl/validators.py
index 6e508c846a8..cfcb1a2d5d2 100644
--- a/ckan/lib/navl/validators.py
+++ b/ckan/lib/navl/validators.py
@@ -117,3 +117,9 @@ def convert_int(value, context):
except ValueError:
raise Invalid(_('Please enter an integer value'))
+def unicode_only(value):
+ '''Accept only unicode values'''
+
+ if not isinstance(value, unicode):
+ raise Invalid(_('Must be a Unicode string value'))
+ return value
diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py
index 0f0067bb901..a3b2b897241 100644
--- a/ckan/lib/plugins.py
+++ b/ckan/lib/plugins.py
@@ -185,16 +185,16 @@ def register_group_plugins(map):
'unfollow', 'admins', 'activity'])))
map.connect('%s_edit' % group_type, '/%s/edit/{id}' % group_type,
controller=group_controller, action='edit',
- ckan_icon='edit')
+ ckan_icon='pencil-square-o')
map.connect('%s_members' % group_type,
'/%s/members/{id}' % group_type,
controller=group_controller,
action='members',
- ckan_icon='group')
+ ckan_icon='users')
map.connect('%s_activity' % group_type,
'/%s/activity/{id}/{offset}' % group_type,
controller=group_controller,
- action='activity', ckan_icon='time'),
+ action='activity', ckan_icon='clock-o'),
map.connect('%s_about' % group_type, '/%s/about/{id}' % group_type,
controller=group_controller,
action='about', ckan_icon='info-sign')
diff --git a/ckan/lib/search/index.py b/ckan/lib/search/index.py
index 878a5fa3a8c..cb341faf910 100644
--- a/ckan/lib/search/index.py
+++ b/ckan/lib/search/index.py
@@ -156,8 +156,8 @@ def index_package(self, pkg_dict, defer_commit=False):
for tag in tags:
if tag.get('vocabulary_id'):
data = {'id': tag['vocabulary_id']}
- vocab = logic.get_action('vocabulary_show')(context, data)
- key = u'vocab_%s' % vocab['name']
+ vocab_name = model.Vocabulary.get(tag['vocabulary_id']).name
+ key = u'vocab_%s' % vocab_name
if key in pkg_dict:
pkg_dict[key].append(tag['name'])
else:
diff --git a/ckan/lib/search/query.py b/ckan/lib/search/query.py
index c136b294ca4..2ab2a63b9c1 100644
--- a/ckan/lib/search/query.py
+++ b/ckan/lib/search/query.py
@@ -20,7 +20,8 @@
VALID_SOLR_PARAMETERS = set([
'q', 'fl', 'fq', 'rows', 'sort', 'start', 'wt', 'qf', 'bf', 'boost',
'facet', 'facet.mincount', 'facet.limit', 'facet.field',
- 'extras', 'fq_list', 'tie', 'defType', 'mm'
+ 'extras', 'fq_list', 'tie', 'defType', 'mm',
+ 'facet.range.end', 'facet.range', 'facet.range.gap', 'facet.range.start'
])
# for (solr) package searches, this specifies the fields that are searched
@@ -359,31 +360,39 @@ def run(self, query):
'Unknown sort order' in e.args[0]:
raise SearchQueryError('Invalid "sort" parameter')
raise SearchError('SOLR returned an error running query: %r Error: %r' %
- (query, e))
- self.count = solr_response.hits
- self.results = solr_response.docs
-
-
- # #1683 Filter out the last row that is sometimes out of order
- self.results = self.results[:rows_to_return]
-
- # get any extras and add to 'extras' dict
- for result in self.results:
- extra_keys = filter(lambda x: x.startswith('extras_'), result.keys())
- extras = {}
- for extra_key in extra_keys:
- value = result.pop(extra_key)
- extras[extra_key[len('extras_'):]] = value
- if extra_keys:
- result['extras'] = extras
-
- # if just fetching the id or name, return a list instead of a dict
- if query.get('fl') in ['id', 'name']:
- self.results = [r.get(query.get('fl')) for r in self.results]
-
- # get facets and convert facets list to a dict
- self.facets = solr_response.facets.get('facet_fields', {})
- for field, values in six.iteritems(self.facets):
- self.facets[field] = dict(zip(values[0::2], values[1::2]))
+ (query, e.reason))
+ try:
+ data = json.loads(solr_response)
+ response = data['response']
+ self.count = response.get('numFound', 0)
+ self.results = response.get('docs', [])
+
+ # #1683 Filter out the last row that is sometimes out of order
+ self.results = self.results[:rows_to_return]
+
+ # get any extras and add to 'extras' dict
+ for result in self.results:
+ extra_keys = filter(lambda x: x.startswith('extras_'), result.keys())
+ extras = {}
+ for extra_key in extra_keys:
+ value = result.pop(extra_key)
+ extras[extra_key[len('extras_'):]] = value
+ if extra_keys:
+ result['extras'] = extras
+
+ # if just fetching the id or name, return a list instead of a dict
+ if query.get('fl') in ['id', 'name']:
+ self.results = [r.get(query.get('fl')) for r in self.results]
+
+ # get facets and convert facets list to a dict
+ self.facets = data.get('facet_counts', {}).get('facet_fields', {})
+ for field, values in self.facets.iteritems():
+ self.facets[field] = dict(zip(values[0::2], values[1::2]))
+ self.facet_ranges = data.get('facet_counts', {}).get('facet_ranges', {})
+ except Exception, e:
+ log.exception(e)
+ raise SearchError(e)
+ finally:
+ conn.close()
return {'results': self.results, 'count': self.count}
diff --git a/ckan/logic/__init__.py b/ckan/logic/__init__.py
index 7f868881477..2f6585d55c2 100644
--- a/ckan/logic/__init__.py
+++ b/ckan/logic/__init__.py
@@ -3,7 +3,7 @@
import functools
import logging
import re
-import sys
+from collections import defaultdict
import formencode.validators
@@ -36,6 +36,7 @@ def __str__(self):
return self.message
+
class NotFound(ActionError):
'''Exception raised by logic functions when a given object is not found.
@@ -308,6 +309,15 @@ def clear_actions_cache():
_actions.clear()
+def chained_action(func):
+ func.chained_action = True
+ return func
+
+
+def _is_chained_action(func):
+ return getattr(func, 'chained_action', False)
+
+
def get_action(action):
'''Return the named :py:mod:`ckan.logic.action` function.
@@ -362,6 +372,7 @@ def get_action(action):
if action not in _actions:
raise KeyError("Action '%s' not found" % action)
return _actions.get(action)
+
# Otherwise look in all the plugins to resolve all possible
# First get the default ones in the ckan/logic/action directory
# Rather than writing them out in full will use __import__
@@ -390,20 +401,32 @@ def get_action(action):
# Then overwrite them with any specific ones in the plugins:
resolved_action_plugins = {}
fetched_actions = {}
+ chained_actions = defaultdict(list)
for plugin in p.PluginImplementations(p.IActions):
for name, auth_function in plugin.get_actions().items():
- if name in resolved_action_plugins:
+ if _is_chained_action(auth_function):
+ chained_actions[name].append(auth_function)
+ elif name in resolved_action_plugins:
raise NameConflict(
'The action %r is already implemented in %r' % (
name,
resolved_action_plugins[name]
)
)
- resolved_action_plugins[name] = plugin.name
- # Extensions are exempted from the auth audit for now
- # This needs to be resolved later
- auth_function.auth_audit_exempt = True
- fetched_actions[name] = auth_function
+ else:
+ resolved_action_plugins[name] = plugin.name
+ # Extensions are exempted from the auth audit for now
+ # This needs to be resolved later
+ auth_function.auth_audit_exempt = True
+ fetched_actions[name] = auth_function
+ for name, func_list in chained_actions.iteritems():
+ if name not in fetched_actions:
+ raise NotFound('The action %r is not found for chained action' % (
+ name))
+ for func in reversed(func_list):
+ prev_func = fetched_actions[name]
+ fetched_actions[name] = functools.partial(func, prev_func)
+
# Use the updated ones in preference to the originals.
_actions.update(fetched_actions)
@@ -574,6 +597,15 @@ def wrapper(context, data_dict):
return wrapper
+def auth_read_safe(action):
+ @functools.wraps(action)
+ def wrapper(context, data_dict):
+ return action(context, data_dict)
+
+ wrapper.auth_read_safe = True
+ return wrapper
+
+
def auth_audit_exempt(action):
''' Dirty hack to stop auth audit being done '''
@functools.wraps(action)
diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py
index 12fc65d7a01..98275d48fa5 100644
--- a/ckan/logic/action/delete.py
+++ b/ckan/logic/action/delete.py
@@ -26,9 +26,7 @@
def user_delete(context, data_dict):
'''Delete a user.
-
Only sysadmins can delete users.
-
:param id: the id or usernamename of the user to delete
:type id: string
'''
@@ -47,13 +45,17 @@ def user_delete(context, data_dict):
if user is None:
raise NotFound('User "{id}" was not found.'.format(id=user_id))
- user.delete()
+ with model.Session.begin_nested():
+ user.delete()
- user_memberships = model.Session.query(model.Member).filter(
- model.Member.table_id == user.id).all()
+ user_memberships = model.Session.query(
+ model.Member
+ ).filter(
+ model.Member.table_id == user.id
+ ).all()
- for membership in user_memberships:
- membership.delete()
+ for membership in user_memberships:
+ membership.delete()
model.repo.commit()
diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py
index 16da4f75d72..ded077e629b 100644
--- a/ckan/logic/action/get.py
+++ b/ckan/logic/action/get.py
@@ -23,7 +23,6 @@
import ckan.plugins as plugins
import ckan.lib.search as search
import ckan.lib.plugins as lib_plugins
-import ckan.lib.activity_streams as activity_streams
import ckan.lib.datapreview as datapreview
import ckan.authz as authz
@@ -1404,6 +1403,8 @@ def user_show(context, data_dict):
:rtype: dictionary
'''
+ _check_access('user_show', context, data_dict)
+
model = context['model']
id = data_dict.get('id', None)
@@ -1418,8 +1419,6 @@ def user_show(context, data_dict):
else:
raise NotFound
- _check_access('user_show', context, data_dict)
-
# include private and draft datasets?
requester = context.get('user')
sysadmin = False
@@ -1780,8 +1779,9 @@ def package_search(context, data_dict):
fl
The parameter that controls which fields are returned in the solr
- query cannot be changed. CKAN always returns the matched datasets as
- dictionary objects.
+ query.
+ fl can be None or a list of result fields, such as ['id', 'extras_custom_field'].
+ if fl = None, datasets are returned as a list of full dictionary.
'''
# sometimes context['schema'] is None
schema = (context.get('schema') or
@@ -1824,8 +1824,12 @@ def package_search(context, data_dict):
else:
data_source = 'validated_data_dict'
data_dict.pop('use_default_schema', None)
- # return a list of package ids
- data_dict['fl'] = 'id {0}'.format(data_source)
+
+ result_fl = data_dict.get('fl')
+ if not result_fl:
+ data_dict['fl'] = 'id {0}'.format(data_source)
+ else:
+ data_dict['fl'] = ' '.join(result_fl)
# we should remove any mention of capacity from the fq and
# instead set it to only retrieve public datasets
@@ -1877,32 +1881,42 @@ def package_search(context, data_dict):
# Add them back so extensions can use them on after_search
data_dict['extras'] = extras
- for package in query.results:
- # get the package object
- package_dict = package.get(data_source)
- ## use data in search index if there
- if package_dict:
- # the package_dict still needs translating when being viewed
- package_dict = json.loads(package_dict)
- if context.get('for_view'):
- for item in plugins.PluginImplementations(
- plugins.IPackageController):
- package_dict = item.before_view(package_dict)
- results.append(package_dict)
- else:
- log.error('No package_dict is coming from solr for package '
- 'id %s', package['id'])
+ if result_fl:
+ for package in query.results:
+ if package.get('extras'):
+ package.update(package['extras'] )
+ package.pop('extras')
+ results.append(package)
+ else:
+ for package in query.results:
+ # get the package object
+ package_dict = package.get(data_source)
+ ## use data in search index if there
+ if package_dict:
+ # the package_dict still needs translating when being viewed
+ package_dict = json.loads(package_dict)
+ if context.get('for_view'):
+ for item in plugins.PluginImplementations(
+ plugins.IPackageController):
+ package_dict = item.before_view(package_dict)
+ results.append(package_dict)
+ else:
+ log.error('No package_dict is coming from solr for package '
+ 'id %s', package['id'])
count = query.count
facets = query.facets
+ facet_ranges = query.facet_ranges
else:
count = 0
facets = {}
+ facet_ranges = None
results = []
search_results = {
'count': count,
'facets': facets,
+ 'facet_ranges': facet_ranges,
'results': results,
'sort': data_dict['sort']
}
@@ -2519,17 +2533,27 @@ def package_activity_list(context, data_dict):
package_ref = data_dict.get('id') # May be name or ID.
package = model.Package.get(package_ref)
+
if package is None:
raise logic.NotFound
offset = int(data_dict.get('offset', 0))
limit = int(
- data_dict.get('limit', config.get('ckan.activity_list_limit', 31)))
+ data_dict.get(
+ 'limit',
+ config.get('ckan.activity_list_limit', 31)
+ )
+ )
- _activity_objects = model.activity.package_activity_list(package.id,
- limit=limit, offset=offset)
- activity_objects = _filter_activity_by_user(_activity_objects,
- _activity_stream_get_filtered_users())
+ _activity_objects = model.activity.package_activity_list(
+ package.id,
+ limit=limit,
+ offset=offset
+ )
+ activity_objects = _filter_activity_by_user(
+ _activity_objects,
+ _activity_stream_get_filtered_users()
+ )
return model_dictize.activity_list_dictize(activity_objects, context)
@@ -2599,10 +2623,12 @@ def organization_activity_list(context, data_dict):
org_show = logic.get_action('organization_show')
org_id = org_show(context, {'id': org_id})['id']
- _activity_objects = model.activity.group_activity_list(org_id,
+ activity_objects = model.activity.group_activity_list(org_id,
limit=limit, offset=offset)
+ """
activity_objects = _filter_activity_by_user(_activity_objects,
_activity_stream_get_filtered_users())
+ """
return model_dictize.activity_list_dictize(activity_objects, context)
@@ -2654,154 +2680,6 @@ def activity_detail_list(context, data_dict):
activity_detail_objects, context)
-def user_activity_list_html(context, data_dict):
- '''Return a user's public activity stream as HTML.
-
- The activity stream is rendered as a snippet of HTML meant to be included
- in an HTML page, i.e. it doesn't have any HTML header or footer.
-
- :param id: The id or name of the user.
- :type id: string
- :param offset: where to start getting activity items from
- (optional, default: 0)
- :type offset: int
- :param limit: the maximum number of activities to return
- (optional, default: 31, the default value is configurable via the
- ckan.activity_list_limit setting)
- :type limit: int
-
- :rtype: string
-
- '''
- activity_stream = user_activity_list(context, data_dict)
- offset = int(data_dict.get('offset', 0))
- extra_vars = {
- 'controller': 'user',
- 'action': 'activity',
- 'id': data_dict['id'],
- 'offset': offset,
- }
- return activity_streams.activity_list_to_html(
- context, activity_stream, extra_vars)
-
-
-def package_activity_list_html(context, data_dict):
- '''Return a package's activity stream as HTML.
-
- The activity stream is rendered as a snippet of HTML meant to be included
- in an HTML page, i.e. it doesn't have any HTML header or footer.
-
- :param id: the id or name of the package
- :type id: string
- :param offset: where to start getting activity items from
- (optional, default: 0)
- :type offset: int
- :param limit: the maximum number of activities to return
- (optional, default: 31, the default value is configurable via the
- ckan.activity_list_limit setting)
- :type limit: int
-
- :rtype: string
-
- '''
- activity_stream = package_activity_list(context, data_dict)
- offset = int(data_dict.get('offset', 0))
- extra_vars = {
- 'controller': 'package',
- 'action': 'activity',
- 'id': data_dict['id'],
- 'offset': offset,
- }
- return activity_streams.activity_list_to_html(
- context, activity_stream, extra_vars)
-
-
-def group_activity_list_html(context, data_dict):
- '''Return a group's activity stream as HTML.
-
- The activity stream is rendered as a snippet of HTML meant to be included
- in an HTML page, i.e. it doesn't have any HTML header or footer.
-
- :param id: the id or name of the group
- :type id: string
- :param offset: where to start getting activity items from
- (optional, default: 0)
- :type offset: int
- :param limit: the maximum number of activities to return
- (optional, default: 31, the default value is configurable via the
- ckan.activity_list_limit setting)
- :type limit: int
-
- :rtype: string
-
- '''
- activity_stream = group_activity_list(context, data_dict)
- offset = int(data_dict.get('offset', 0))
- extra_vars = {
- 'controller': 'group',
- 'action': 'activity',
- 'id': data_dict['id'],
- 'offset': offset,
- }
- return activity_streams.activity_list_to_html(
- context, activity_stream, extra_vars)
-
-
-def organization_activity_list_html(context, data_dict):
- '''Return a organization's activity stream as HTML.
-
- The activity stream is rendered as a snippet of HTML meant to be included
- in an HTML page, i.e. it doesn't have any HTML header or footer.
-
- :param id: the id or name of the organization
- :type id: string
-
- :rtype: string
-
- '''
- activity_stream = organization_activity_list(context, data_dict)
- offset = int(data_dict.get('offset', 0))
- extra_vars = {
- 'controller': 'organization',
- 'action': 'activity',
- 'id': data_dict['id'],
- 'offset': offset,
- }
-
- return activity_streams.activity_list_to_html(
- context, activity_stream, extra_vars)
-
-
-def recently_changed_packages_activity_list_html(context, data_dict):
- '''Return the activity stream of all recently changed packages as HTML.
-
- The activity stream includes all recently added or changed packages. It is
- rendered as a snippet of HTML meant to be included in an HTML page, i.e. it
- doesn't have any HTML header or footer.
-
- :param offset: where to start getting activity items from
- (optional, default: 0)
- :type offset: int
- :param limit: the maximum number of activities to return
- (optional, default: 31, the default value is configurable via the
- ckan.activity_list_limit setting)
- :type limit: int
-
- :rtype: string
-
- '''
- activity_stream = recently_changed_packages_activity_list(
- context, data_dict)
- offset = int(data_dict.get('offset', 0))
- extra_vars = {
- 'controller': 'package',
- 'action': 'activity',
- 'offset': offset,
- }
- return activity_streams.activity_list_to_html(
- context, activity_stream, extra_vars)
-
-
def _follower_count(context, data_dict, default_schema, ModelClass):
schema = context.get('schema', default_schema)
data_dict, errors = _validate(data_dict, schema, context)
@@ -3330,37 +3208,6 @@ def dashboard_activity_list(context, data_dict):
return activity_dicts
-@logic.validate(ckan.logic.schema.default_pagination_schema)
-def dashboard_activity_list_html(context, data_dict):
- '''Return the authorized (via login or API key) user's dashboard activity
- stream as HTML.
-
- The activity stream is rendered as a snippet of HTML meant to be included
- in an HTML page, i.e. it doesn't have any HTML header or footer.
-
- :param offset: where to start getting activity items from
- (optional, default: 0)
- :type offset: int
- :param limit: the maximum number of activities to return
- (optional, default: 31, the default value is configurable via the
- ckan.activity_list_limit setting)
- :type limit: int
-
- :rtype: string
-
- '''
- activity_stream = dashboard_activity_list(context, data_dict)
- model = context['model']
- offset = data_dict.get('offset', 0)
- extra_vars = {
- 'controller': 'user',
- 'action': 'dashboard',
- 'offset': offset,
- }
- return activity_streams.activity_list_to_html(context, activity_stream,
- extra_vars)
-
-
def dashboard_new_activities_count(context, data_dict):
'''Return the number of new activities in the user's dashboard.
diff --git a/ckan/logic/action/update.py b/ckan/logic/action/update.py
index db451f5b887..f8d08bb90c1 100644
--- a/ckan/logic/action/update.py
+++ b/ckan/logic/action/update.py
@@ -27,6 +27,7 @@
from ckan.common import _, request
+from ckan import new_authz
log = logging.getLogger(__name__)
@@ -619,7 +620,7 @@ def user_update(context, data_dict):
'''Update a user account.
Normal users can only update their own user accounts. Sysadmins can update
- any user account.
+ any user account. Can not modify exisiting user's name.
For further parameters see
:py:func:`~ckan.logic.action.create.user_create`.
diff --git a/ckan/logic/auth/get.py b/ckan/logic/auth/get.py
index 011b19c2e84..5abb443a92e 100644
--- a/ckan/logic/auth/get.py
+++ b/ckan/logic/auth/get.py
@@ -3,8 +3,11 @@
import ckan.logic as logic
import ckan.authz as authz
from ckan.lib.base import _
-from ckan.logic.auth import (get_package_object, get_group_object,
- get_resource_object)
+from ckan.logic.auth import (
+ get_package_object,
+ get_group_object,
+ get_resource_object
+)
def sysadmin(context, data_dict):
@@ -12,6 +15,7 @@ def sysadmin(context, data_dict):
return {'success': False, 'msg': _('Not authorized')}
+@logic.auth_read_safe
def site_read(context, data_dict):
"""\
This function should be deprecated. It is only here because we couldn't
@@ -23,72 +27,103 @@ def site_read(context, data_dict):
# FIXME we need to remove this for now we allow site read
return {'success': True}
+
+@logic.auth_read_safe
def package_search(context, data_dict):
# Everyone can search by default
return {'success': True}
+
+@logic.auth_read_safe
def package_list(context, data_dict):
# List of all active packages are visible by default
return {'success': True}
+@logic.auth_read_safe
def current_package_list_with_resources(context, data_dict):
return authz.is_authorized('package_list', context, data_dict)
+
+@logic.auth_read_safe
def revision_list(context, data_dict):
# In our new model everyone can read the revison list
return {'success': True}
+@logic.auth_read_safe
def group_revision_list(context, data_dict):
return authz.is_authorized('group_show', context, data_dict)
+
+@logic.auth_read_safe
def organization_revision_list(context, data_dict):
return authz.is_authorized('group_show', context, data_dict)
+
+@logic.auth_read_safe
def package_revision_list(context, data_dict):
return authz.is_authorized('package_show', context, data_dict)
+
+@logic.auth_read_safe
def group_list(context, data_dict):
# List of all active groups is visible by default
return {'success': True}
+@logic.auth_read_safe
def group_list_authz(context, data_dict):
return authz.is_authorized('group_list', context, data_dict)
+
+@logic.auth_read_safe
def group_list_available(context, data_dict):
return authz.is_authorized('group_list', context, data_dict)
+
+@logic.auth_read_safe
def organization_list(context, data_dict):
# List of all active organizations are visible by default
return {'success': True}
+
+@logic.auth_read_safe
def organization_list_for_user(context, data_dict):
return {'success': True}
+
+@logic.auth_read_safe
def license_list(context, data_dict):
# Licenses list is visible by default
return {'success': True}
+
+@logic.auth_read_safe
def vocabulary_list(context, data_dict):
# List of all vocabularies are visible by default
return {'success': True}
+
+@logic.auth_read_safe
def tag_list(context, data_dict):
# Tags list is visible by default
return {'success': True}
+
+@logic.auth_read_safe
def user_list(context, data_dict):
# Users list is visible by default
return {'success': True}
+
+@logic.auth_read_safe
def package_relationships_list(context, data_dict):
user = context.get('user')
@@ -105,18 +140,26 @@ def package_relationships_list(context, data_dict):
authorized2 = True
if not (authorized1 and authorized2):
- return {'success': False, 'msg': _('User %s not authorized to read these packages') % user}
+ return {
+ 'success': False,
+ 'msg': _('User %s not authorized to read these packages') % user
+ }
else:
return {'success': True}
+
+@logic.auth_read_safe
def package_show(context, data_dict):
user = context.get('user')
package = get_package_object(context, data_dict)
# draft state indicates package is still in the creation process
# so we need to check we have creation rights.
if package.state.startswith('draft'):
- auth = authz.is_authorized('package_update',
- context, data_dict)
+ auth = authz.is_authorized(
+ 'package_update',
+ context,
+ data_dict
+ )
authorized = auth.get('success')
elif package.owner_org is None and package.state == 'active':
return {'success': True}
@@ -127,11 +170,23 @@ def package_show(context, data_dict):
authorized = authz.has_user_permission_for_group_or_org(
package.owner_org, user, 'read')
if not authorized:
- return {'success': False, 'msg': _('User %s not authorized to read package %s') % (user, package.id)}
+ return {
+ 'success': False,
+ 'msg': _('User %s not authorized to read package %s') % (
+ user,
+ package.id
+ )
+ }
else:
return {'success': True}
+@logic.auth_read_safe
+def related_show(context, data_dict=None):
+ return {'success': True}
+
+
+@logic.auth_read_safe
def resource_show(context, data_dict):
model = context['model']
user = context.get('user')
@@ -140,29 +195,48 @@ def resource_show(context, data_dict):
# check authentication against package
pkg = model.Package.get(resource.package_id)
if not pkg:
- raise logic.NotFound(_('No package found for this resource, cannot check auth.'))
+ raise logic.NotFound(
+ _('No package found for this resource, cannot check auth.')
+ )
pkg_dict = {'id': pkg.id}
- authorized = authz.is_authorized('package_show', context, pkg_dict).get('success')
+ authorized = authz.is_authorized(
+ 'package_show',
+ context,
+ pkg_dict
+ ).get('success')
if not authorized:
- return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (user, resource.id)}
+ return {
+ 'success': False,
+ 'msg': _('User %s not authorized to read resource %s') % (
+ user,
+ resource.id
+ )
+ }
else:
return {'success': True}
+@logic.auth_read_safe
def resource_view_show(context, data_dict):
return authz.is_authorized('resource_show', context, data_dict)
+
+@logic.auth_read_safe
def resource_view_list(context, data_dict):
return authz.is_authorized('resource_show', context, data_dict)
+
+@logic.auth_read_safe
def revision_show(context, data_dict):
# No authz check in the logic function
return {'success': True}
+
+@logic.auth_read_safe
def group_show(context, data_dict):
user = context.get('user')
group = get_group_object(context, data_dict)
@@ -173,70 +247,105 @@ def group_show(context, data_dict):
if authorized:
return {'success': True}
else:
- return {'success': False, 'msg': _('User %s not authorized to read group %s') % (user, group.id)}
+ return {
+ 'success': False,
+ 'msg': _('User %s not authorized to read group %s') % (
+ user,
+ group.id
+ )
+ }
+@logic.auth_read_safe
def organization_show(context, data_dict):
return authz.is_authorized('group_show', context, data_dict)
+
+@logic.auth_read_safe
def vocabulary_show(context, data_dict):
# Allow viewing of vocabs by default
return {'success': True}
+
+@logic.auth_read_safe
def tag_show(context, data_dict):
# No authz check in the logic function
return {'success': True}
+
+@logic.auth_read_safe
def user_show(context, data_dict):
# By default, user details can be read by anyone, but some properties like
# the API key are stripped at the action level if not not logged in.
return {'success': True}
+@logic.auth_read_safe
def package_autocomplete(context, data_dict):
return authz.is_authorized('package_list', context, data_dict)
+
+@logic.auth_read_safe
def group_autocomplete(context, data_dict):
return authz.is_authorized('group_list', context, data_dict)
+
+@logic.auth_read_safe
def organization_autocomplete(context, data_dict):
return authz.is_authorized('organization_list', context, data_dict)
+
+@logic.auth_read_safe
def tag_autocomplete(context, data_dict):
return authz.is_authorized('tag_list', context, data_dict)
+
+@logic.auth_read_safe
def user_autocomplete(context, data_dict):
return authz.is_authorized('user_list', context, data_dict)
+
+@logic.auth_read_safe
def format_autocomplete(context, data_dict):
return {'success': True}
+
+@logic.auth_read_safe
def task_status_show(context, data_dict):
return {'success': True}
+
+@logic.auth_read_safe
def resource_status_show(context, data_dict):
return {'success': True}
## Modifications for rest api
+@logic.auth_read_safe
def package_show_rest(context, data_dict):
return authz.is_authorized('package_show', context, data_dict)
+
+@logic.auth_read_safe
def group_show_rest(context, data_dict):
return authz.is_authorized('group_show', context, data_dict)
+
+@logic.auth_read_safe
def tag_show_rest(context, data_dict):
return authz.is_authorized('tag_show', context, data_dict)
+
+@logic.auth_read_safe
def get_site_user(context, data_dict):
# FIXME this is available to sysadmins currently till
# @auth_sysadmins_check decorator is added
@@ -244,10 +353,12 @@ def get_site_user(context, data_dict):
'msg': 'Only internal services allowed to use this action'}
+@logic.auth_read_safe
def member_roles_list(context, data_dict):
return {'success': True}
+@logic.auth_read_safe
def dashboard_activity_list(context, data_dict):
# FIXME: context['user'] could be an IP address but that case is not
# handled here. Maybe add an auth helper function like is_logged_in().
@@ -258,30 +369,39 @@ def dashboard_activity_list(context, data_dict):
'msg': _("You must be logged in to access your dashboard.")}
+@logic.auth_read_safe
def dashboard_new_activities_count(context, data_dict):
# FIXME: This should go through check_access() not call is_authorized()
# directly, but wait until 2939-orgs is merged before fixing this.
# This is so a better not authourized message can be sent.
- return authz.is_authorized('dashboard_activity_list',
- context, data_dict)
+ return authz.is_authorized(
+ 'dashboard_activity_list',
+ context,
+ data_dict
+ )
+@logic.auth_read_safe
def user_follower_list(context, data_dict):
return authz.is_authorized('sysadmin', context, data_dict)
+@logic.auth_read_safe
def dataset_follower_list(context, data_dict):
return authz.is_authorized('sysadmin', context, data_dict)
+@logic.auth_read_safe
def group_follower_list(context, data_dict):
return authz.is_authorized('sysadmin', context, data_dict)
+@logic.auth_read_safe
def organization_follower_list(context, data_dict):
return authz.is_authorized('sysadmin', context, data_dict)
+@logic.auth_read_safe
def _followee_list(context, data_dict):
model = context['model']
@@ -299,47 +419,57 @@ def _followee_list(context, data_dict):
return authz.is_authorized('sysadmin', context, data_dict)
+@logic.auth_read_safe
def followee_list(context, data_dict):
return _followee_list(context, data_dict)
+@logic.auth_read_safe
@logic.auth_audit_exempt
def user_followee_list(context, data_dict):
return _followee_list(context, data_dict)
+@logic.auth_read_safe
@logic.auth_audit_exempt
def dataset_followee_list(context, data_dict):
return _followee_list(context, data_dict)
+@logic.auth_read_safe
@logic.auth_audit_exempt
def group_followee_list(context, data_dict):
return _followee_list(context, data_dict)
+@logic.auth_read_safe
@logic.auth_audit_exempt
def organization_followee_list(context, data_dict):
return _followee_list(context, data_dict)
+@logic.auth_read_safe
def user_reset(context, data_dict):
return {'success': True}
+@logic.auth_read_safe
def request_reset(context, data_dict):
return {'success': True}
+@logic.auth_read_safe
def help_show(context, data_dict):
return {'success': True}
+@logic.auth_read_safe
def config_option_show(context, data_dict):
'''Show runtime-editable configuration option. Only sysadmins.'''
return {'success': False}
+@logic.auth_read_safe
def config_option_list(context, data_dict):
'''List runtime-editable configuration options. Only sysadmins.'''
return {'success': False}
diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
index 78ac4ed5a40..e1f5f3b86b6 100644
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -588,6 +588,7 @@ def default_autocomplete_schema():
def default_package_search_schema():
schema = {
'q': [ignore_missing, unicode],
+ 'fl': [ignore_missing, list_of_strings],
'fq': [ignore_missing, unicode],
'rows': [ignore_missing, natural_number_validator],
'sort': [ignore_missing, unicode],
diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py
index ad8a91c5a8c..863b8689f5f 100644
--- a/ckan/logic/validators.py
+++ b/ckan/logic/validators.py
@@ -544,10 +544,9 @@ def user_name_validator(key, data, errors, context):
raise Invalid(_('User names must be strings'))
user = model.User.get(new_user_name)
+ user_obj_from_context = context.get('user_obj')
if user is not None:
# A user with new_user_name already exists in the database.
-
- user_obj_from_context = context.get('user_obj')
if user_obj_from_context and user_obj_from_context.id == user.id:
# If there's a user_obj in context with the same id as the user
# found in the db, then we must be doing a user_update and not
@@ -558,6 +557,12 @@ def user_name_validator(key, data, errors, context):
# name, so you can create a new user with that name or update an
# existing user's name to that name.
errors[key].append(_('That login name is not available.'))
+ elif user_obj_from_context:
+ old_user = model.User.get(user_obj_from_context.id)
+ if old_user is not None and old_user.state != model.State.PENDING:
+ errors[key].append(_('That login name can not be modified.'))
+ else:
+ return
def user_both_passwords_entered(key, data, errors, context):
diff --git a/ckan/model/activity.py b/ckan/model/activity.py
index cfd6d02e2a5..f165f7fb5b7 100644
--- a/ckan/model/activity.py
+++ b/ckan/model/activity.py
@@ -3,7 +3,17 @@
import datetime
from sqlalchemy import (
- orm, types, Column, Table, ForeignKey, desc, or_, union_all)
+ orm,
+ types,
+ Column,
+ Table,
+ ForeignKey,
+ desc,
+ or_,
+ and_,
+ union_all,
+ func
+)
import ckan.model
import meta
@@ -127,6 +137,15 @@ def _user_activity_query(user_id, limit):
q2 = _activities_limit(_activities_about_user_query(user_id), limit)
return _activities_union_all(q1, q2)
+def _package_activity_query(package_id):
+ '''Return an SQLAlchemy query for all activities about package_id.
+
+ '''
+ import ckan.model as model
+ q = model.Session.query(model.Activity)
+ q = q.filter_by(object_id=package_id)
+ return q
+
def user_activity_list(user_id, limit, offset):
'''Return user_id's public activity stream.
@@ -143,16 +162,6 @@ def user_activity_list(user_id, limit, offset):
return _activities_at_offset(q, limit, offset)
-def _package_activity_query(package_id):
- '''Return an SQLAlchemy query for all activities about package_id.
-
- '''
- import ckan.model as model
- q = model.Session.query(model.Activity)
- q = q.filter_by(object_id=package_id)
- return q
-
-
def package_activity_list(package_id, limit, offset):
'''Return the given dataset (package)'s public activity stream.
@@ -164,8 +173,26 @@ def package_activity_list(package_id, limit, offset):
etc.
'''
- q = _package_activity_query(package_id)
- return _activities_at_offset(q, limit, offset)
+ import ckan.model as model
+
+ q = model.Session.query(
+ model.Activity
+ ).filter_by(
+ object_id=package_id
+ )
+
+ if offset:
+ q = q.filter(
+ model.Activity.timestamp < func.to_timestamp(offset)
+ )
+
+ q = q.order_by(
+ model.Activity.timestamp.desc()
+ ).limit(
+ limit
+ )
+
+ return q
def _group_activity_query(group_id):
@@ -182,14 +209,33 @@ def _group_activity_query(group_id):
# Return a query with no results.
return model.Session.query(model.Activity).filter("0=1")
- dataset_ids = [dataset.id for dataset in group.packages()]
+ q = model.Session.query(
+ model.Activity
+ ).outerjoin(
+ model.Member,
+ and_(
+ model.Activity.object_id == model.Member.table_id,
+ model.Member.state == 'active'
+ )
+ ).outerjoin(
+ model.Package,
+ and_(
+ model.Package.id == model.Member.table_id,
+ model.Package.private == False,
+ model.Package.state == 'active'
+ )
+ ).filter(
+ # We only care about activity either on the the group itself or on
+ # packages within that group.
+ # FIXME: This means that activity that occured while a package belonged
+ # to a group but was then removed will not show up. This may not be
+ # desired but is consistent with legacy behaviour.
+ or_(
+ model.Member.group_id == group_id,
+ model.Activity.object_id == group_id
+ ),
+ )
- q = model.Session.query(model.Activity)
- if dataset_ids:
- q = q.filter(or_(model.Activity.object_id == group_id,
- model.Activity.object_id.in_(dataset_ids)))
- else:
- q = q.filter(model.Activity.object_id == group_id)
return q
diff --git a/ckan/model/package.py b/ckan/model/package.py
index 7b9b9528ec9..a67306e975c 100644
--- a/ckan/model/package.py
+++ b/ckan/model/package.py
@@ -1,11 +1,10 @@
# encoding: utf-8
import datetime
-from calendar import timegm
import logging
logger = logging.getLogger(__name__)
-from sqlalchemy.sql import select, and_, union, or_
+from sqlalchemy.sql import and_, or_
from sqlalchemy import orm
from sqlalchemy import types, Column, Table
from ckan.common import config
@@ -24,7 +23,7 @@
__all__ = ['Package', 'package_table', 'package_revision_table',
'PACKAGE_NAME_MAX_LENGTH', 'PACKAGE_NAME_MIN_LENGTH',
- 'PACKAGE_VERSION_MAX_LENGTH', 'PackageTag', 'PackageTagRevision',
+ 'PACKAGE_VERSION_MAX_LENGTH', 'PackageTagRevision',
'PackageRevision']
@@ -510,6 +509,7 @@ def get_fields(core_only=False, fields_to_ignore=None):
def activity_stream_item(self, activity_type, revision, user_id):
import ckan.model
import ckan.logic
+
assert activity_type in ("new", "changed"), (
str(activity_type))
@@ -529,10 +529,17 @@ def activity_stream_item(self, activity_type, revision, user_id):
activity_type = 'deleted'
try:
- d = {'package': dictization.table_dictize(self,
- context={'model': ckan.model})}
- return activity.Activity(user_id, self.id, revision.id,
- "%s package" % activity_type, d)
+ # We save the entire rendered package dict so we can support
+ # viewing the past packages from the activity feed.
+ dictized_package = ckan.logic.get_action('package_show')({
+ 'model': ckan.model,
+ 'session': ckan.model.Session,
+ 'for_view': True,
+ 'ignore_auth': True
+ }, {
+ 'id': self.id,
+ 'include_tracking': True
+ })
except ckan.logic.NotFound:
# This happens if this package is being purged and therefore has no
# current revision.
@@ -540,6 +547,22 @@ def activity_stream_item(self, activity_type, revision, user_id):
# is purged.
return None
+ actor = meta.Session.query(ckan.model.User).get(user_id)
+
+ return activity.Activity(
+ user_id,
+ self.id,
+ revision.id,
+ "%s package" % activity_type,
+ {
+ 'package': dictized_package,
+ # We keep the acting user name around so that actions can be
+ # properly displayed even if the user is deleted in the future.
+ # Legacy tests do not include valid users :(
+ 'actor': actor.name if actor else None
+ }
+ )
+
def activity_stream_detail(self, activity_id, activity_type):
import ckan.model
diff --git a/ckan/model/resource.py b/ckan/model/resource.py
index aa608d46eb9..529a7694401 100644
--- a/ckan/model/resource.py
+++ b/ckan/model/resource.py
@@ -189,7 +189,6 @@ def activity_stream_detail(self, activity_id, activity_type):
),
)
},
-order_by=[resource_table.c.package_id],
extension=[vdm.sqlalchemy.Revisioner(resource_revision_table),
extension.PluginMapperExtension(),
],
diff --git a/ckan/model/user.py b/ckan/model/user.py
index 83b1c5b3df7..4e6e89a9a3f 100644
--- a/ckan/model/user.py
+++ b/ckan/model/user.py
@@ -9,7 +9,7 @@
from passlib.hash import pbkdf2_sha512
from sqlalchemy.sql.expression import or_
from sqlalchemy.orm import synonym
-from sqlalchemy import types, Column, Table
+from sqlalchemy import types, Column, Table, func
import vdm.sqlalchemy
import meta
@@ -189,21 +189,45 @@ def as_dict(self):
def number_of_edits(self):
# have to import here to avoid circular imports
import ckan.model as model
- revisions_q = meta.Session.query(model.Revision)
- revisions_q = revisions_q.filter_by(author=self.name)
- return revisions_q.count()
+
+ # Get count efficiently without spawning the SQLAlchemy subquery
+ # wrapper. Reset the VDM-forced order_by on timestamp.
+ return meta.Session.execute(
+ meta.Session.query(
+ model.Revision
+ ).filter_by(
+ author=self.name
+ ).statement.with_only_columns(
+ [func.count()]
+ ).order_by(
+ None
+ )
+ ).scalar()
def number_created_packages(self, include_private_and_draft=False):
# have to import here to avoid circular imports
import ckan.model as model
- q = meta.Session.query(model.Package)\
- .filter_by(creator_user_id=self.id)
+
+ # Get count efficiently without spawning the SQLAlchemy subquery
+ # wrapper. Reset the VDM-forced order_by on timestamp.
+ q = meta.Session.query(
+ model.Package
+ ).filter_by(
+ creator_user_id=self.id
+ )
+
if include_private_and_draft:
q = q.filter(model.Package.state != 'deleted')
else:
- q = q.filter_by(state='active')\
- .filter_by(private=False)
- return q.count()
+ q = q.filter_by(state='active', private=False)
+
+ return meta.Session.execute(
+ q.statement.with_only_columns(
+ [func.count()]
+ ).order_by(
+ None
+ )
+ ).scalar()
def activate(self):
''' Activate the user '''
diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py
index 2651c394a87..bcf4a92eb4e 100644
--- a/ckan/plugins/interfaces.py
+++ b/ckan/plugins/interfaces.py
@@ -233,7 +233,7 @@ def info(self):
:param default_description: default description that will be used if
the view is created automatically (optional, defaults to '').
:param icon: icon for the view type. Should be one of the
- `Font Awesome`_ types without the `icon-` prefix eg. `compass`
+ `Font Awesome`_ types without the `fa fa-` prefix eg. `compass`
(optional, defaults to 'picture').
:param always_available: the view type should be always available when
creating new views regardless of the format of the resource
@@ -264,7 +264,7 @@ def info(self):
'schema': {
'image_url': [ignore_empty, unicode]
},
- 'icon': 'picture',
+ 'icon': 'picture-o',
'always_available': True,
'iframed': False,
}
@@ -797,6 +797,17 @@ def get_actions(self):
By decorating a function with the `ckan.logic.side_effect_free`
decorator, the associated action will be made available by a GET
request (as well as the usual POST request) through the action API.
+
+ By decrorating a function with the 'ckan.plugins.toolkit.chained_action,
+ the action will be chained to another function defined in plugins with a
+ "first plugin wins" pattern, which means the first plugin declaring a
+ chained action should be called first. Chained actions must be
+ defined as action_function(original_action, context, data_dict)
+ where the first parameter will be set to the action function in
+ the next plugin or in core ckan. The chained action may call the
+ original_action function, optionally passing different values,
+ handling exceptions, returning different values and/or raising
+ different exceptions to the caller.
"""
diff --git a/ckan/plugins/toolkit.py b/ckan/plugins/toolkit.py
index 899763806ea..41c9eeb3cf7 100644
--- a/ckan/plugins/toolkit.py
+++ b/ckan/plugins/toolkit.py
@@ -224,6 +224,7 @@ def _initialize(self):
t['literal'] = webhelpers.html.tags.literal
t['get_action'] = logic.get_action
+ t['chained_action'] = logic.chained_action
t['get_converter'] = logic.get_validator # For backwards compatibility
t['get_validator'] = logic.get_validator
t['check_access'] = logic.check_access
diff --git a/ckan/public/base/css/fuchsia.css b/ckan/public/base/css/fuchsia.css
index 5b71168593b..7d30bd4147e 100644
--- a/ckan/public/base/css/fuchsia.css
+++ b/ckan/public/base/css/fuchsia.css
@@ -2298,8 +2298,8 @@ button.close {
-moz-border-radius: 6px;
border-radius: 6px;
}
-.btn-large [class^="icon-"],
-.btn-large [class*=" icon-"] {
+.btn-large [class^="fa fa-"],
+.btn-large [class*=" fa fa-"] {
margin-top: 4px;
}
.btn-small {
@@ -2309,12 +2309,12 @@ button.close {
-moz-border-radius: 3px;
border-radius: 3px;
}
-.btn-small [class^="icon-"],
-.btn-small [class*=" icon-"] {
+.btn-small [class^="fa fa-"],
+.btn-small [class*=" fa fa-"] {
margin-top: 0;
}
-.btn-mini [class^="icon-"],
-.btn-mini [class*=" icon-"] {
+.btn-mini [class^="fa fa-"],
+.btn-mini [class*=" fa fa-"] {
margin-top: -1px;
}
.btn-mini {
@@ -2915,8 +2915,8 @@ input[type="submit"].btn.btn-mini {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
background-color: #e73892;
}
-.nav-list [class^="icon-"],
-.nav-list [class*=" icon-"] {
+.nav-list [class^="fa fa-"],
+.nav-list [class*=" fa fa-"] {
margin-right: 2px;
}
.nav-list .divider {
@@ -3483,7 +3483,7 @@ input[type="submit"].btn.btn-mini {
.navbar .btn-navbar.active {
background-color: #cccccc \9;
}
-.navbar .btn-navbar .icon-bar {
+.navbar .btn-navbar .fa-bar {
display: block;
width: 18px;
height: 2px;
@@ -3495,7 +3495,7 @@ input[type="submit"].btn.btn-mini {
-moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
}
-.btn-navbar .icon-bar + .icon-bar {
+.btn-navbar .fa-bar + .fa-bar {
margin-top: 3px;
}
.navbar .nav > li > .dropdown-menu:before {
@@ -6376,8 +6376,8 @@ textarea {
.form-horizontal .info-inline:before {
top: 8px;
}
-.info-block .icon-large,
-.info-inline .icon-large {
+.info-block .fa-lg,
+.info-inline .fa-lg {
float: left;
font-size: 22px;
margin-right: 15px;
@@ -6912,7 +6912,7 @@ textarea {
-moz-border-radius: 100px;
border-radius: 100px;
}
-.js .image-upload .btn-remove-url .icon-remove {
+.js .image-upload .btn-remove-url .fa-times {
margin-right: 0;
}
.add-member-form .control-label {
@@ -6975,7 +6975,7 @@ textarea {
margin-right: 10px;
text-transform: uppercase;
}
-.dataset-private .icon-lock {
+.dataset-private .fa-lock {
width: 9px;
}
.dataset-private.pull-right {
@@ -7908,39 +7908,39 @@ h4 small {
height: 35px;
background-position: -320px -62px;
}
-[class^="icon-"],
-[class*=" icon-"] {
+[class^=" fa fa-"],
+[class*=" fa fa-"] {
display: inline-block;
text-align: right;
font-size: 14px;
line-height: 1;
width: 14px;
}
-.btn [class^="icon-"],
-.nav [class^="icon-"],
-.module-heading [class^="icon-"],
-.dropdown [class^="icon-"],
-.btn [class*=" icon-"],
-.nav [class*=" icon-"],
-.module-heading [class*=" icon-"],
-.dropdown [class*=" icon-"] {
+.btn [class^="fa fa-"],
+.nav [class^="fa fa-"],
+.module-heading [class^="fa fa-"],
+.dropdown [class^="fa fa-"],
+.btn [class*=" fa fa-"],
+.nav [class*=" fa fa-"],
+.module-heading [class*=" fa fa-"],
+.dropdown [class*=" fa fa-"] {
margin-right: 4px;
}
-.info-block [class^="icon-"],
-.info-block [class*=" icon-"] {
+.info-block [class^="fa fa-"],
+.info-block [class*=" fa fa-"] {
float: left;
font-size: 28px;
width: 28px;
margin-right: 5px;
margin-top: 2px;
}
-.breadcrumb .home .icon-home {
+.breadcrumb .home .fa-home {
font-size: 24px;
width: 24px;
vertical-align: -1px;
}
-.info-block-small [class^="icon-"],
-.info-block-small [class*=" icon-"] {
+.info-block-small [class^="fa fa-"],
+.info-block-small [class*=" fa fa-"] {
font-size: 14px;
width: 14px;
margin-top: 1px;
@@ -8522,13 +8522,13 @@ h4 small {
text-shadow: none;
margin-top: 15px;
}
-.masthead .btn-navbar .icon-bar,
-.masthead .btn-navbar:hover .icon-bar,
-.masthead .btn-navbar:focus .icon-bar,
-.masthead .btn-navbar:active .icon-bar,
-.masthead .btn-navbar.active .icon-bar,
-.masthead .btn-navbar.disabled .icon-bar,
-.masthead .btn-navbar[disabled] .icon-bar {
+.masthead .btn-navbar .fa-bar,
+.masthead .btn-navbar:hover .fa-bar,
+.masthead .btn-navbar:focus .fa-bar,
+.masthead .btn-navbar:active .fa-bar,
+.masthead .btn-navbar.active .fa-bar,
+.masthead .btn-navbar.disabled .fa-bar,
+.masthead .btn-navbar[disabled] .fa-bar {
margin-right: 0;
}
.masthead .debug {
@@ -8659,13 +8659,13 @@ h4 small {
text-shadow: none;
margin-top: 15px;
}
-.site-footer .btn-navbar .icon-bar,
-.site-footer .btn-navbar:hover .icon-bar,
-.site-footer .btn-navbar:focus .icon-bar,
-.site-footer .btn-navbar:active .icon-bar,
-.site-footer .btn-navbar.active .icon-bar,
-.site-footer .btn-navbar.disabled .icon-bar,
-.site-footer .btn-navbar[disabled] .icon-bar {
+.site-footer .btn-navbar .fa-bar,
+.site-footer .btn-navbar:hover .fa-bar,
+.site-footer .btn-navbar:focus .fa-bar,
+.site-footer .btn-navbar:active .fa-bar,
+.site-footer .btn-navbar.active .fa-bar,
+.site-footer .btn-navbar.disabled .fa-bar,
+.site-footer .btn-navbar[disabled] .fa-bar {
margin-right: 0;
}
.site-footer .debug {
@@ -9313,197 +9313,25 @@ iframe {
white-space: nowrap;
}
.ie9 .homepage .media.module-heading .media-image img,
-.ie8 .homepage .media.module-heading .media-image img,
-.ie7 .homepage .media.module-heading .media-image img {
+.ie8 .homepage .media.module-heading .media-image img {
width: 85px !important;
}
-.ie8 .masthead .nav-collapse,
-.ie7 .masthead .nav-collapse {
+.ie8 .masthead .nav-collapse {
float: right;
}
.ie8 [role=main],
-.ie7 [role=main],
-.ie8 .main,
-.ie7 .main {
+.ie8 .main {
padding-top: 10px;
background: #eeeeee url("../../../base/images/bg.png");
}
-.ie8 .hero,
-.ie7 .hero {
+.ie8 .hero {
background: url("../../../base/images/background-tile.png");
}
-.ie8 .hero .hero-primary.module-popup .box,
-.ie7 .hero .hero-primary.module-popup .box {
+.ie8 .hero .hero-primary.module-popup .box {
padding-bottom: 20px !important;
margin-bottom: 0 !important;
}
-.ie8 .lang-dropdown,
-.ie7 .lang-dropdown {
+.ie8 .lang-dropdown {
position: relative !important;
top: -90px !important;
}
-.ie7 .alert {
- position: relative;
-}
-.ie7 .alert .close {
- position: absolute;
- top: 6px !important;
- right: 20px;
-}
-.ie7 .media-item {
- width: 30%;
-}
-.ie7 .tags .tag-list {
- *zoom: 1;
-}
-.ie7 .tags .tag-list:before,
-.ie7 .tags .tag-list:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .tags .tag-list:after {
- clear: both;
-}
-.ie7 .tags .tag-list li {
- display: block;
- float: left;
-}
-.ie7 .tags h3 {
- float: left;
-}
-.ie7 .tags .tag {
- display: block;
-}
-.ie7 .search-giant input {
- width: 95%;
-}
-.ie7 .control-full input,
-.ie7 .control-full select,
-.ie7 .control-full textarea {
- width: 95%;
-}
-.ie7 .control-full.control-large .controls input {
- padding-bottom: 20px;
-}
-.ie7 .controls {
- position: relative;
-}
-.ie7 .controls .info-block,
-.ie7 .controls .info-inline {
- position: absolute;
- top: 0;
- right: 0;
-}
-.ie7 .form-horizontal .controls {
- margin-left: 0;
-}
-.ie7 .control-custom .checkbox {
- *display: inline;
- /* IE7 inline-block hack */
- *zoom: 1;
-}
-.ie7 .stages {
- overflow: hidden;
- background-color: #ededed;
-}
-.ie7 .stages li {
- height: 30px;
- width: 27.5%;
-}
-.ie7 .stages li button,
-.ie7 .stages li span {
- display: block;
- height: 30px;
- padding-left: 20px;
-}
-.ie7 .stages li button {
- height: 50px;
-}
-.ie7 .stages li .highlight {
- width: auto;
-}
-.ie7 .account-masthead .account a i {
- line-height: 31px;
-}
-.ie7 .masthead {
- position: relative;
- z-index: 1;
-}
-.ie7 .masthead .logo img,
-.ie7 .masthead nav {
- *display: inline;
- /* IE7 inline-block hack */
- *zoom: 1;
-}
-.ie7 .masthead .header-image {
- display: block;
-}
-.ie7 .masthead .account .dropdown-menu {
- z-index: 10000;
-}
-.ie7 .module-narrow .nav-item.image {
- *zoom: 1;
-}
-.ie7 .module-narrow .nav-item.image:before,
-.ie7 .module-narrow .nav-item.image:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .module-narrow .nav-item.image:after {
- clear: both;
-}
-.ie7 .nav-facet .nav-item.active a {
- content: 'x';
-}
-.ie7 .toolbar .breadcrumb li {
- padding-right: 10px;
- margin-right: 5px;
- background: transparent url("../../../base/images/breadcrumb-slash-ie7.png") 100% 50% no-repeat;
-}
-.ie7 .toolbar .breadcrumb li.active {
- background-image: none;
-}
-.ie7 .module-heading {
- *zoom: 1;
- position: relative;
-}
-.ie7 .module-heading:before,
-.ie7 .module-heading:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .module-heading:after {
- clear: both;
-}
-.ie7 .module-heading .media-content {
- position: relative;
-}
-.ie7 .module-heading .media-image img {
- float: left;
-}
-.ie7 .group-listing {
- position: relative;
- zoom: 1;
-}
-.ie7 .resource-item {
- position: static;
- padding-bottom: 1px;
-}
-.ie7 .resource-item .heading {
- position: relative;
-}
-.ie7 .resource-item .format-label {
- left: -48px;
-}
-.ie7 .resource-item .btn-group {
- position: relative;
- float: right;
- top: -35px;
- right: 0;
-}
-.ie7 .media-overlay .media-heading {
- background-color: #000;
-}
diff --git a/ckan/public/base/css/green.css b/ckan/public/base/css/green.css
index cf1eca19a31..1e88c493b21 100644
--- a/ckan/public/base/css/green.css
+++ b/ckan/public/base/css/green.css
@@ -2298,8 +2298,8 @@ button.close {
-moz-border-radius: 6px;
border-radius: 6px;
}
-.btn-large [class^="icon-"],
-.btn-large [class*=" icon-"] {
+.btn-large [class^="fa fa-"],
+.btn-large [class*=" fa fa-"] {
margin-top: 4px;
}
.btn-small {
@@ -2309,12 +2309,12 @@ button.close {
-moz-border-radius: 3px;
border-radius: 3px;
}
-.btn-small [class^="icon-"],
-.btn-small [class*=" icon-"] {
+.btn-small [class^="fa fa-"],
+.btn-small [class*=" fa fa-"] {
margin-top: 0;
}
-.btn-mini [class^="icon-"],
-.btn-mini [class*=" icon-"] {
+.btn-mini [class^="fa fa-"],
+.btn-mini [class*=" fa fa-"] {
margin-top: -1px;
}
.btn-mini {
@@ -2915,8 +2915,8 @@ input[type="submit"].btn.btn-mini {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
background-color: #2f9b45;
}
-.nav-list [class^="icon-"],
-.nav-list [class*=" icon-"] {
+.nav-list [class^="fa fa-"],
+.nav-list [class*=" fa fa-"] {
margin-right: 2px;
}
.nav-list .divider {
@@ -3483,7 +3483,7 @@ input[type="submit"].btn.btn-mini {
.navbar .btn-navbar.active {
background-color: #cccccc \9;
}
-.navbar .btn-navbar .icon-bar {
+.navbar .btn-navbar .fa-bar {
display: block;
width: 18px;
height: 2px;
@@ -3495,7 +3495,7 @@ input[type="submit"].btn.btn-mini {
-moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
}
-.btn-navbar .icon-bar + .icon-bar {
+.btn-navbar .fa-bar + .fa-bar {
margin-top: 3px;
}
.navbar .nav > li > .dropdown-menu:before {
@@ -6376,8 +6376,8 @@ textarea {
.form-horizontal .info-inline:before {
top: 8px;
}
-.info-block .icon-large,
-.info-inline .icon-large {
+.info-block .fa-lg,
+.info-inline .fa-lg {
float: left;
font-size: 22px;
margin-right: 15px;
@@ -6912,7 +6912,7 @@ textarea {
-moz-border-radius: 100px;
border-radius: 100px;
}
-.js .image-upload .btn-remove-url .icon-remove {
+.js .image-upload .btn-remove-url .fa-times {
margin-right: 0;
}
.add-member-form .control-label {
@@ -6975,7 +6975,7 @@ textarea {
margin-right: 10px;
text-transform: uppercase;
}
-.dataset-private .icon-lock {
+.dataset-private .fa-lock {
width: 9px;
}
.dataset-private.pull-right {
@@ -7908,39 +7908,39 @@ h4 small {
height: 35px;
background-position: -320px -62px;
}
-[class^="icon-"],
-[class*=" icon-"] {
+[class^="fa fa-"],
+[class*=" fa fa-"] {
display: inline-block;
text-align: right;
font-size: 14px;
line-height: 1;
width: 14px;
}
-.btn [class^="icon-"],
-.nav [class^="icon-"],
-.module-heading [class^="icon-"],
-.dropdown [class^="icon-"],
-.btn [class*=" icon-"],
-.nav [class*=" icon-"],
-.module-heading [class*=" icon-"],
-.dropdown [class*=" icon-"] {
+.btn [class^="fa fa-"],
+.nav [class^="fa fa-"],
+.module-heading [class^="fa fa-"],
+.dropdown [class^="fa fa-"],
+.btn [class*=" fa fa-"],
+.nav [class*=" fa fa-"],
+.module-heading [class*=" fa fa-"],
+.dropdown [class*=" fa fa-"] {
margin-right: 4px;
}
-.info-block [class^="icon-"],
-.info-block [class*=" icon-"] {
+.info-block [class^="fa fa-"],
+.info-block [class*=" fa fa-"] {
float: left;
font-size: 28px;
width: 28px;
margin-right: 5px;
margin-top: 2px;
}
-.breadcrumb .home .icon-home {
+.breadcrumb .home .fa-home {
font-size: 24px;
width: 24px;
vertical-align: -1px;
}
-.info-block-small [class^="icon-"],
-.info-block-small [class*=" icon-"] {
+.info-block-small [class^="fa fa-"],
+.info-block-small [class*=" fa fa-"] {
font-size: 14px;
width: 14px;
margin-top: 1px;
@@ -8522,13 +8522,13 @@ h4 small {
text-shadow: none;
margin-top: 15px;
}
-.masthead .btn-navbar .icon-bar,
-.masthead .btn-navbar:hover .icon-bar,
-.masthead .btn-navbar:focus .icon-bar,
-.masthead .btn-navbar:active .icon-bar,
-.masthead .btn-navbar.active .icon-bar,
-.masthead .btn-navbar.disabled .icon-bar,
-.masthead .btn-navbar[disabled] .icon-bar {
+.masthead .btn-navbar .fa-bar,
+.masthead .btn-navbar:hover .fa-bar,
+.masthead .btn-navbar:focus .fa-bar,
+.masthead .btn-navbar:active .fa-bar,
+.masthead .btn-navbar.active .fa-bar,
+.masthead .btn-navbar.disabled .fa-bar,
+.masthead .btn-navbar[disabled] .fa-bar {
margin-right: 0;
}
.masthead .debug {
@@ -8659,13 +8659,13 @@ h4 small {
text-shadow: none;
margin-top: 15px;
}
-.site-footer .btn-navbar .icon-bar,
-.site-footer .btn-navbar:hover .icon-bar,
-.site-footer .btn-navbar:focus .icon-bar,
-.site-footer .btn-navbar:active .icon-bar,
-.site-footer .btn-navbar.active .icon-bar,
-.site-footer .btn-navbar.disabled .icon-bar,
-.site-footer .btn-navbar[disabled] .icon-bar {
+.site-footer .btn-navbar .fa-bar,
+.site-footer .btn-navbar:hover .fa-bar,
+.site-footer .btn-navbar:focus .fa-bar,
+.site-footer .btn-navbar:active .fa-bar,
+.site-footer .btn-navbar.active .fa-bar,
+.site-footer .btn-navbar.disabled .fa-bar,
+.site-footer .btn-navbar[disabled] .fa-bar {
margin-right: 0;
}
.site-footer .debug {
@@ -9313,197 +9313,25 @@ iframe {
white-space: nowrap;
}
.ie9 .homepage .media.module-heading .media-image img,
-.ie8 .homepage .media.module-heading .media-image img,
-.ie7 .homepage .media.module-heading .media-image img {
+.ie8 .homepage .media.module-heading .media-image img {
width: 85px !important;
}
-.ie8 .masthead .nav-collapse,
-.ie7 .masthead .nav-collapse {
+.ie8 .masthead .nav-collapse {
float: right;
}
.ie8 [role=main],
-.ie7 [role=main],
-.ie8 .main,
-.ie7 .main {
+.ie8 .main {
padding-top: 10px;
background: #eeeeee url("../../../base/images/bg.png");
}
-.ie8 .hero,
-.ie7 .hero {
+.ie8 .hero {
background: url("../../../base/images/background-tile.png");
}
-.ie8 .hero .hero-primary.module-popup .box,
-.ie7 .hero .hero-primary.module-popup .box {
+.ie8 .hero .hero-primary.module-popup .box {
padding-bottom: 20px !important;
margin-bottom: 0 !important;
}
-.ie8 .lang-dropdown,
-.ie7 .lang-dropdown {
+.ie8 .lang-dropdown {
position: relative !important;
top: -90px !important;
}
-.ie7 .alert {
- position: relative;
-}
-.ie7 .alert .close {
- position: absolute;
- top: 6px !important;
- right: 20px;
-}
-.ie7 .media-item {
- width: 30%;
-}
-.ie7 .tags .tag-list {
- *zoom: 1;
-}
-.ie7 .tags .tag-list:before,
-.ie7 .tags .tag-list:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .tags .tag-list:after {
- clear: both;
-}
-.ie7 .tags .tag-list li {
- display: block;
- float: left;
-}
-.ie7 .tags h3 {
- float: left;
-}
-.ie7 .tags .tag {
- display: block;
-}
-.ie7 .search-giant input {
- width: 95%;
-}
-.ie7 .control-full input,
-.ie7 .control-full select,
-.ie7 .control-full textarea {
- width: 95%;
-}
-.ie7 .control-full.control-large .controls input {
- padding-bottom: 20px;
-}
-.ie7 .controls {
- position: relative;
-}
-.ie7 .controls .info-block,
-.ie7 .controls .info-inline {
- position: absolute;
- top: 0;
- right: 0;
-}
-.ie7 .form-horizontal .controls {
- margin-left: 0;
-}
-.ie7 .control-custom .checkbox {
- *display: inline;
- /* IE7 inline-block hack */
- *zoom: 1;
-}
-.ie7 .stages {
- overflow: hidden;
- background-color: #ededed;
-}
-.ie7 .stages li {
- height: 30px;
- width: 27.5%;
-}
-.ie7 .stages li button,
-.ie7 .stages li span {
- display: block;
- height: 30px;
- padding-left: 20px;
-}
-.ie7 .stages li button {
- height: 50px;
-}
-.ie7 .stages li .highlight {
- width: auto;
-}
-.ie7 .account-masthead .account a i {
- line-height: 31px;
-}
-.ie7 .masthead {
- position: relative;
- z-index: 1;
-}
-.ie7 .masthead .logo img,
-.ie7 .masthead nav {
- *display: inline;
- /* IE7 inline-block hack */
- *zoom: 1;
-}
-.ie7 .masthead .header-image {
- display: block;
-}
-.ie7 .masthead .account .dropdown-menu {
- z-index: 10000;
-}
-.ie7 .module-narrow .nav-item.image {
- *zoom: 1;
-}
-.ie7 .module-narrow .nav-item.image:before,
-.ie7 .module-narrow .nav-item.image:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .module-narrow .nav-item.image:after {
- clear: both;
-}
-.ie7 .nav-facet .nav-item.active a {
- content: 'x';
-}
-.ie7 .toolbar .breadcrumb li {
- padding-right: 10px;
- margin-right: 5px;
- background: transparent url("../../../base/images/breadcrumb-slash-ie7.png") 100% 50% no-repeat;
-}
-.ie7 .toolbar .breadcrumb li.active {
- background-image: none;
-}
-.ie7 .module-heading {
- *zoom: 1;
- position: relative;
-}
-.ie7 .module-heading:before,
-.ie7 .module-heading:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .module-heading:after {
- clear: both;
-}
-.ie7 .module-heading .media-content {
- position: relative;
-}
-.ie7 .module-heading .media-image img {
- float: left;
-}
-.ie7 .group-listing {
- position: relative;
- zoom: 1;
-}
-.ie7 .resource-item {
- position: static;
- padding-bottom: 1px;
-}
-.ie7 .resource-item .heading {
- position: relative;
-}
-.ie7 .resource-item .format-label {
- left: -48px;
-}
-.ie7 .resource-item .btn-group {
- position: relative;
- float: right;
- top: -35px;
- right: 0;
-}
-.ie7 .media-overlay .media-heading {
- background-color: #000;
-}
diff --git a/ckan/public/base/css/main.css b/ckan/public/base/css/main.css
index 937787aab93..637aa850fe8 100644
--- a/ckan/public/base/css/main.css
+++ b/ckan/public/base/css/main.css
@@ -2298,8 +2298,8 @@ button.close {
-moz-border-radius: 6px;
border-radius: 6px;
}
-.btn-large [class^="icon-"],
-.btn-large [class*=" icon-"] {
+.btn-large [class^="fa fa-"],
+.btn-large [class*=" fa fa-"] {
margin-top: 4px;
}
.btn-small {
@@ -2309,12 +2309,12 @@ button.close {
-moz-border-radius: 3px;
border-radius: 3px;
}
-.btn-small [class^="icon-"],
-.btn-small [class*=" icon-"] {
+.btn-small [class^="fa fa-"],
+.btn-small [class*=" fa fa-"] {
margin-top: 0;
}
-.btn-mini [class^="icon-"],
-.btn-mini [class*=" icon-"] {
+.btn-mini [class^="fa fa-"],
+.btn-mini [class*=" fa fa-"] {
margin-top: -1px;
}
.btn-mini {
@@ -2915,8 +2915,8 @@ input[type="submit"].btn.btn-mini {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
background-color: #187794;
}
-.nav-list [class^="icon-"],
-.nav-list [class*=" icon-"] {
+.nav-list [class^="fa fa-"],
+.nav-list [class*=" fa fa-"] {
margin-right: 2px;
}
.nav-list .divider {
@@ -3483,7 +3483,7 @@ input[type="submit"].btn.btn-mini {
.navbar .btn-navbar.active {
background-color: #cccccc \9;
}
-.navbar .btn-navbar .icon-bar {
+.navbar .btn-navbar .fa-bar {
display: block;
width: 18px;
height: 2px;
@@ -3495,7 +3495,7 @@ input[type="submit"].btn.btn-mini {
-moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
}
-.btn-navbar .icon-bar + .icon-bar {
+.btn-navbar .fa-bar + .fa-bar {
margin-top: 3px;
}
.navbar .nav > li > .dropdown-menu:before {
@@ -6376,8 +6376,8 @@ textarea {
.form-horizontal .info-inline:before {
top: 8px;
}
-.info-block .icon-large,
-.info-inline .icon-large {
+.info-block .fa-lg,
+.info-inline .fa-lg {
float: left;
font-size: 22px;
margin-right: 15px;
@@ -6912,7 +6912,7 @@ textarea {
-moz-border-radius: 100px;
border-radius: 100px;
}
-.js .image-upload .btn-remove-url .icon-remove {
+.js .image-upload .btn-remove-url .fa-times {
margin-right: 0;
}
.add-member-form .control-label {
@@ -6975,7 +6975,7 @@ textarea {
margin-right: 10px;
text-transform: uppercase;
}
-.dataset-private .icon-lock {
+.dataset-private .fa-lock {
width: 9px;
}
.dataset-private.pull-right {
@@ -7908,39 +7908,40 @@ h4 small {
height: 35px;
background-position: -320px -62px;
}
-[class^="icon-"],
-[class*=" icon-"] {
+
+[class^="fa fa-"],
+[class*=" fa fa-"] {
display: inline-block;
text-align: right;
font-size: 14px;
line-height: 1;
width: 14px;
}
-.btn [class^="icon-"],
-.nav [class^="icon-"],
-.module-heading [class^="icon-"],
-.dropdown [class^="icon-"],
-.btn [class*=" icon-"],
-.nav [class*=" icon-"],
-.module-heading [class*=" icon-"],
-.dropdown [class*=" icon-"] {
+.btn [class^="fa fa-"],
+.nav [class^="fa fa-"],
+.module-heading [class^="fa fa-"],
+.dropdown [class^="fa fa-"],
+.btn [class*=" fa fa-"],
+.nav [class*=" fa fa-"],
+.module-heading [class*=" fa fa-"],
+.dropdown [class*=" fa fa-"] {
margin-right: 4px;
}
-.info-block [class^="icon-"],
-.info-block [class*=" icon-"] {
+.info-block [class^="fa fa-"],
+.info-block [class*=" fa fa-"] {
float: left;
font-size: 28px;
width: 28px;
margin-right: 5px;
margin-top: 2px;
}
-.breadcrumb .home .icon-home {
+.breadcrumb .home .fa-home {
font-size: 24px;
width: 24px;
vertical-align: -1px;
}
-.info-block-small [class^="icon-"],
-.info-block-small [class*=" icon-"] {
+.info-block-small [class^="fa fa-"],
+.info-block-small [class*=" fa fa-"] {
font-size: 14px;
width: 14px;
margin-top: 1px;
@@ -8522,13 +8523,13 @@ h4 small {
text-shadow: none;
margin-top: 15px;
}
-.masthead .btn-navbar .icon-bar,
-.masthead .btn-navbar:hover .icon-bar,
-.masthead .btn-navbar:focus .icon-bar,
-.masthead .btn-navbar:active .icon-bar,
-.masthead .btn-navbar.active .icon-bar,
-.masthead .btn-navbar.disabled .icon-bar,
-.masthead .btn-navbar[disabled] .icon-bar {
+.masthead .btn-navbar .fa-bar,
+.masthead .btn-navbar:hover .fa-bar,
+.masthead .btn-navbar:focus .fa-bar,
+.masthead .btn-navbar:active .fa-bar,
+.masthead .btn-navbar.active .fa-bar,
+.masthead .btn-navbar.disabled .fa-bar,
+.masthead .btn-navbar[disabled] .fa-bar {
margin-right: 0;
}
.masthead .debug {
@@ -8659,13 +8660,13 @@ h4 small {
text-shadow: none;
margin-top: 15px;
}
-.site-footer .btn-navbar .icon-bar,
-.site-footer .btn-navbar:hover .icon-bar,
-.site-footer .btn-navbar:focus .icon-bar,
-.site-footer .btn-navbar:active .icon-bar,
-.site-footer .btn-navbar.active .icon-bar,
-.site-footer .btn-navbar.disabled .icon-bar,
-.site-footer .btn-navbar[disabled] .icon-bar {
+.site-footer .btn-navbar .fa-bar,
+.site-footer .btn-navbar:hover .fa-bar,
+.site-footer .btn-navbar:focus .fa-bar,
+.site-footer .btn-navbar:active .fa-bar,
+.site-footer .btn-navbar.active .fa-bar,
+.site-footer .btn-navbar.disabled .fa-bar,
+.site-footer .btn-navbar[disabled] .fa-bar {
margin-right: 0;
}
.site-footer .debug {
@@ -9313,197 +9314,25 @@ iframe {
white-space: nowrap;
}
.ie9 .homepage .media.module-heading .media-image img,
-.ie8 .homepage .media.module-heading .media-image img,
-.ie7 .homepage .media.module-heading .media-image img {
+.ie8 .homepage .media.module-heading .media-image img {
width: 85px !important;
}
-.ie8 .masthead .nav-collapse,
-.ie7 .masthead .nav-collapse {
+.ie8 .masthead .nav-collapse {
float: right;
}
.ie8 [role=main],
-.ie7 [role=main],
-.ie8 .main,
-.ie7 .main {
+.ie8 .main {
padding-top: 10px;
background: #eeeeee url("../../../base/images/bg.png");
}
-.ie8 .hero,
-.ie7 .hero {
+.ie8 .hero {
background: url("../../../base/images/background-tile.png");
}
-.ie8 .hero .hero-primary.module-popup .box,
-.ie7 .hero .hero-primary.module-popup .box {
+.ie8 .hero .hero-primary.module-popup .box {
padding-bottom: 20px !important;
margin-bottom: 0 !important;
}
-.ie8 .lang-dropdown,
-.ie7 .lang-dropdown {
+.ie8 .lang-dropdown {
position: relative !important;
top: -90px !important;
}
-.ie7 .alert {
- position: relative;
-}
-.ie7 .alert .close {
- position: absolute;
- top: 6px !important;
- right: 20px;
-}
-.ie7 .media-item {
- width: 30%;
-}
-.ie7 .tags .tag-list {
- *zoom: 1;
-}
-.ie7 .tags .tag-list:before,
-.ie7 .tags .tag-list:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .tags .tag-list:after {
- clear: both;
-}
-.ie7 .tags .tag-list li {
- display: block;
- float: left;
-}
-.ie7 .tags h3 {
- float: left;
-}
-.ie7 .tags .tag {
- display: block;
-}
-.ie7 .search-giant input {
- width: 95%;
-}
-.ie7 .control-full input,
-.ie7 .control-full select,
-.ie7 .control-full textarea {
- width: 95%;
-}
-.ie7 .control-full.control-large .controls input {
- padding-bottom: 20px;
-}
-.ie7 .controls {
- position: relative;
-}
-.ie7 .controls .info-block,
-.ie7 .controls .info-inline {
- position: absolute;
- top: 0;
- right: 0;
-}
-.ie7 .form-horizontal .controls {
- margin-left: 0;
-}
-.ie7 .control-custom .checkbox {
- *display: inline;
- /* IE7 inline-block hack */
- *zoom: 1;
-}
-.ie7 .stages {
- overflow: hidden;
- background-color: #ededed;
-}
-.ie7 .stages li {
- height: 30px;
- width: 27.5%;
-}
-.ie7 .stages li button,
-.ie7 .stages li span {
- display: block;
- height: 30px;
- padding-left: 20px;
-}
-.ie7 .stages li button {
- height: 50px;
-}
-.ie7 .stages li .highlight {
- width: auto;
-}
-.ie7 .account-masthead .account a i {
- line-height: 31px;
-}
-.ie7 .masthead {
- position: relative;
- z-index: 1;
-}
-.ie7 .masthead .logo img,
-.ie7 .masthead nav {
- *display: inline;
- /* IE7 inline-block hack */
- *zoom: 1;
-}
-.ie7 .masthead .header-image {
- display: block;
-}
-.ie7 .masthead .account .dropdown-menu {
- z-index: 10000;
-}
-.ie7 .module-narrow .nav-item.image {
- *zoom: 1;
-}
-.ie7 .module-narrow .nav-item.image:before,
-.ie7 .module-narrow .nav-item.image:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .module-narrow .nav-item.image:after {
- clear: both;
-}
-.ie7 .nav-facet .nav-item.active a {
- content: 'x';
-}
-.ie7 .toolbar .breadcrumb li {
- padding-right: 10px;
- margin-right: 5px;
- background: transparent url("../../../base/images/breadcrumb-slash-ie7.png") 100% 50% no-repeat;
-}
-.ie7 .toolbar .breadcrumb li.active {
- background-image: none;
-}
-.ie7 .module-heading {
- *zoom: 1;
- position: relative;
-}
-.ie7 .module-heading:before,
-.ie7 .module-heading:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .module-heading:after {
- clear: both;
-}
-.ie7 .module-heading .media-content {
- position: relative;
-}
-.ie7 .module-heading .media-image img {
- float: left;
-}
-.ie7 .group-listing {
- position: relative;
- zoom: 1;
-}
-.ie7 .resource-item {
- position: static;
- padding-bottom: 1px;
-}
-.ie7 .resource-item .heading {
- position: relative;
-}
-.ie7 .resource-item .format-label {
- left: -48px;
-}
-.ie7 .resource-item .btn-group {
- position: relative;
- float: right;
- top: -35px;
- right: 0;
-}
-.ie7 .media-overlay .media-heading {
- background-color: #000;
-}
diff --git a/ckan/public/base/css/maroon.css b/ckan/public/base/css/maroon.css
index 5c1515ca9c2..e8489a70022 100644
--- a/ckan/public/base/css/maroon.css
+++ b/ckan/public/base/css/maroon.css
@@ -2298,8 +2298,8 @@ button.close {
-moz-border-radius: 6px;
border-radius: 6px;
}
-.btn-large [class^="icon-"],
-.btn-large [class*=" icon-"] {
+.btn-large [class^="fa fa-"],
+.btn-large [class*=" fa fa-"] {
margin-top: 4px;
}
.btn-small {
@@ -2309,12 +2309,12 @@ button.close {
-moz-border-radius: 3px;
border-radius: 3px;
}
-.btn-small [class^="icon-"],
-.btn-small [class*=" icon-"] {
+.btn-small [class^="fa fa-"],
+.btn-small [class*=" fa fa-"] {
margin-top: 0;
}
-.btn-mini [class^="icon-"],
-.btn-mini [class*=" icon-"] {
+.btn-mini [class^="fa fa-"],
+.btn-mini [class*=" fa fa-"] {
margin-top: -1px;
}
.btn-mini {
@@ -2915,8 +2915,8 @@ input[type="submit"].btn.btn-mini {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
background-color: #810606;
}
-.nav-list [class^="icon-"],
-.nav-list [class*=" icon-"] {
+.nav-list [class^="fa fa-"],
+.nav-list [class*=" fa fa-"] {
margin-right: 2px;
}
.nav-list .divider {
@@ -3483,7 +3483,7 @@ input[type="submit"].btn.btn-mini {
.navbar .btn-navbar.active {
background-color: #cccccc \9;
}
-.navbar .btn-navbar .icon-bar {
+.navbar .btn-navbar .fa-bar {
display: block;
width: 18px;
height: 2px;
@@ -3495,7 +3495,7 @@ input[type="submit"].btn.btn-mini {
-moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
}
-.btn-navbar .icon-bar + .icon-bar {
+.btn-navbar .fa-bar + .fa-bar {
margin-top: 3px;
}
.navbar .nav > li > .dropdown-menu:before {
@@ -6376,8 +6376,8 @@ textarea {
.form-horizontal .info-inline:before {
top: 8px;
}
-.info-block .icon-large,
-.info-inline .icon-large {
+.info-block .fa-lg,
+.info-inline .fa-lg {
float: left;
font-size: 22px;
margin-right: 15px;
@@ -6912,7 +6912,7 @@ textarea {
-moz-border-radius: 100px;
border-radius: 100px;
}
-.js .image-upload .btn-remove-url .icon-remove {
+.js .image-upload .btn-remove-url .fa-times {
margin-right: 0;
}
.add-member-form .control-label {
@@ -6975,7 +6975,7 @@ textarea {
margin-right: 10px;
text-transform: uppercase;
}
-.dataset-private .icon-lock {
+.dataset-private .fa-lock {
width: 9px;
}
.dataset-private.pull-right {
@@ -7908,39 +7908,39 @@ h4 small {
height: 35px;
background-position: -320px -62px;
}
-[class^="icon-"],
-[class*=" icon-"] {
+[class^="fa fa-"],
+[class*=" fa fa-"] {
display: inline-block;
text-align: right;
font-size: 14px;
line-height: 1;
width: 14px;
}
-.btn [class^="icon-"],
-.nav [class^="icon-"],
-.module-heading [class^="icon-"],
-.dropdown [class^="icon-"],
-.btn [class*=" icon-"],
-.nav [class*=" icon-"],
-.module-heading [class*=" icon-"],
-.dropdown [class*=" icon-"] {
+.btn [class^="fa fa-"],
+.nav [class^="fa fa-"],
+.module-heading [class^="fa fa-"],
+.dropdown [class^="fa fa-"],
+.btn [class*=" fa fa-"],
+.nav [class*=" fa fa-"],
+.module-heading [class*=" fa fa-"],
+.dropdown [class*=" fa fa-"] {
margin-right: 4px;
}
-.info-block [class^="icon-"],
-.info-block [class*=" icon-"] {
+.info-block [class^="fa fa-"],
+.info-block [class*=" fa fa-"] {
float: left;
font-size: 28px;
width: 28px;
margin-right: 5px;
margin-top: 2px;
}
-.breadcrumb .home .icon-home {
+.breadcrumb .home .fa-home {
font-size: 24px;
width: 24px;
vertical-align: -1px;
}
-.info-block-small [class^="icon-"],
-.info-block-small [class*=" icon-"] {
+.info-block-small [class^="fa fa-"],
+.info-block-small [class*=" fa fa-"] {
font-size: 14px;
width: 14px;
margin-top: 1px;
@@ -8522,13 +8522,13 @@ h4 small {
text-shadow: none;
margin-top: 15px;
}
-.masthead .btn-navbar .icon-bar,
-.masthead .btn-navbar:hover .icon-bar,
-.masthead .btn-navbar:focus .icon-bar,
-.masthead .btn-navbar:active .icon-bar,
-.masthead .btn-navbar.active .icon-bar,
-.masthead .btn-navbar.disabled .icon-bar,
-.masthead .btn-navbar[disabled] .icon-bar {
+.masthead .btn-navbar .fa-bar,
+.masthead .btn-navbar:hover .fa-bar,
+.masthead .btn-navbar:focus .fa-bar,
+.masthead .btn-navbar:active .fa-bar,
+.masthead .btn-navbar.active .fa-bar,
+.masthead .btn-navbar.disabled .fa-bar,
+.masthead .btn-navbar[disabled] .fa-bar {
margin-right: 0;
}
.masthead .debug {
@@ -8659,13 +8659,13 @@ h4 small {
text-shadow: none;
margin-top: 15px;
}
-.site-footer .btn-navbar .icon-bar,
-.site-footer .btn-navbar:hover .icon-bar,
-.site-footer .btn-navbar:focus .icon-bar,
-.site-footer .btn-navbar:active .icon-bar,
-.site-footer .btn-navbar.active .icon-bar,
-.site-footer .btn-navbar.disabled .icon-bar,
-.site-footer .btn-navbar[disabled] .icon-bar {
+.site-footer .btn-navbar .fa-bar,
+.site-footer .btn-navbar:hover .fa-bar,
+.site-footer .btn-navbar:focus .fa-bar,
+.site-footer .btn-navbar:active .fa-bar,
+.site-footer .btn-navbar.active .fa-bar,
+.site-footer .btn-navbar.disabled .fa-bar,
+.site-footer .btn-navbar[disabled] .fa-bar {
margin-right: 0;
}
.site-footer .debug {
@@ -9313,197 +9313,25 @@ iframe {
white-space: nowrap;
}
.ie9 .homepage .media.module-heading .media-image img,
-.ie8 .homepage .media.module-heading .media-image img,
-.ie7 .homepage .media.module-heading .media-image img {
+.ie8 .homepage .media.module-heading .media-image img {
width: 85px !important;
}
-.ie8 .masthead .nav-collapse,
-.ie7 .masthead .nav-collapse {
+.ie8 .masthead .nav-collapse {
float: right;
}
.ie8 [role=main],
-.ie7 [role=main],
-.ie8 .main,
-.ie7 .main {
+.ie8 .main {
padding-top: 10px;
background: #eeeeee url("../../../base/images/bg.png");
}
-.ie8 .hero,
-.ie7 .hero {
+.ie8 .hero {
background: url("../../../base/images/background-tile.png");
}
-.ie8 .hero .hero-primary.module-popup .box,
-.ie7 .hero .hero-primary.module-popup .box {
+.ie8 .hero .hero-primary.module-popup .box {
padding-bottom: 20px !important;
margin-bottom: 0 !important;
}
-.ie8 .lang-dropdown,
-.ie7 .lang-dropdown {
+.ie8 .lang-dropdown {
position: relative !important;
top: -90px !important;
}
-.ie7 .alert {
- position: relative;
-}
-.ie7 .alert .close {
- position: absolute;
- top: 6px !important;
- right: 20px;
-}
-.ie7 .media-item {
- width: 30%;
-}
-.ie7 .tags .tag-list {
- *zoom: 1;
-}
-.ie7 .tags .tag-list:before,
-.ie7 .tags .tag-list:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .tags .tag-list:after {
- clear: both;
-}
-.ie7 .tags .tag-list li {
- display: block;
- float: left;
-}
-.ie7 .tags h3 {
- float: left;
-}
-.ie7 .tags .tag {
- display: block;
-}
-.ie7 .search-giant input {
- width: 95%;
-}
-.ie7 .control-full input,
-.ie7 .control-full select,
-.ie7 .control-full textarea {
- width: 95%;
-}
-.ie7 .control-full.control-large .controls input {
- padding-bottom: 20px;
-}
-.ie7 .controls {
- position: relative;
-}
-.ie7 .controls .info-block,
-.ie7 .controls .info-inline {
- position: absolute;
- top: 0;
- right: 0;
-}
-.ie7 .form-horizontal .controls {
- margin-left: 0;
-}
-.ie7 .control-custom .checkbox {
- *display: inline;
- /* IE7 inline-block hack */
- *zoom: 1;
-}
-.ie7 .stages {
- overflow: hidden;
- background-color: #ededed;
-}
-.ie7 .stages li {
- height: 30px;
- width: 27.5%;
-}
-.ie7 .stages li button,
-.ie7 .stages li span {
- display: block;
- height: 30px;
- padding-left: 20px;
-}
-.ie7 .stages li button {
- height: 50px;
-}
-.ie7 .stages li .highlight {
- width: auto;
-}
-.ie7 .account-masthead .account a i {
- line-height: 31px;
-}
-.ie7 .masthead {
- position: relative;
- z-index: 1;
-}
-.ie7 .masthead .logo img,
-.ie7 .masthead nav {
- *display: inline;
- /* IE7 inline-block hack */
- *zoom: 1;
-}
-.ie7 .masthead .header-image {
- display: block;
-}
-.ie7 .masthead .account .dropdown-menu {
- z-index: 10000;
-}
-.ie7 .module-narrow .nav-item.image {
- *zoom: 1;
-}
-.ie7 .module-narrow .nav-item.image:before,
-.ie7 .module-narrow .nav-item.image:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .module-narrow .nav-item.image:after {
- clear: both;
-}
-.ie7 .nav-facet .nav-item.active a {
- content: 'x';
-}
-.ie7 .toolbar .breadcrumb li {
- padding-right: 10px;
- margin-right: 5px;
- background: transparent url("../../../base/images/breadcrumb-slash-ie7.png") 100% 50% no-repeat;
-}
-.ie7 .toolbar .breadcrumb li.active {
- background-image: none;
-}
-.ie7 .module-heading {
- *zoom: 1;
- position: relative;
-}
-.ie7 .module-heading:before,
-.ie7 .module-heading:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .module-heading:after {
- clear: both;
-}
-.ie7 .module-heading .media-content {
- position: relative;
-}
-.ie7 .module-heading .media-image img {
- float: left;
-}
-.ie7 .group-listing {
- position: relative;
- zoom: 1;
-}
-.ie7 .resource-item {
- position: static;
- padding-bottom: 1px;
-}
-.ie7 .resource-item .heading {
- position: relative;
-}
-.ie7 .resource-item .format-label {
- left: -48px;
-}
-.ie7 .resource-item .btn-group {
- position: relative;
- float: right;
- top: -35px;
- right: 0;
-}
-.ie7 .media-overlay .media-heading {
- background-color: #000;
-}
diff --git a/ckan/public/base/css/red.css b/ckan/public/base/css/red.css
index e1d29a42c52..d32e4fe0f6e 100644
--- a/ckan/public/base/css/red.css
+++ b/ckan/public/base/css/red.css
@@ -2298,8 +2298,8 @@ button.close {
-moz-border-radius: 6px;
border-radius: 6px;
}
-.btn-large [class^="icon-"],
-.btn-large [class*=" icon-"] {
+.btn-large [class^="fa fa-"],
+.btn-large [class*=" fa fa-"] {
margin-top: 4px;
}
.btn-small {
@@ -2309,12 +2309,12 @@ button.close {
-moz-border-radius: 3px;
border-radius: 3px;
}
-.btn-small [class^="icon-"],
-.btn-small [class*=" icon-"] {
+.btn-small [class^="fa fa-"],
+.btn-small [class*=" fa fa-"] {
margin-top: 0;
}
-.btn-mini [class^="icon-"],
-.btn-mini [class*=" icon-"] {
+.btn-mini [class^="fa fa-"],
+.btn-mini [class*=" fa fa-"] {
margin-top: -1px;
}
.btn-mini {
@@ -2915,8 +2915,8 @@ input[type="submit"].btn.btn-mini {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
background-color: #c14531;
}
-.nav-list [class^="icon-"],
-.nav-list [class*=" icon-"] {
+.nav-list [class^="fa fa-"],
+.nav-list [class*=" fa fa-"] {
margin-right: 2px;
}
.nav-list .divider {
@@ -3483,7 +3483,7 @@ input[type="submit"].btn.btn-mini {
.navbar .btn-navbar.active {
background-color: #cccccc \9;
}
-.navbar .btn-navbar .icon-bar {
+.navbar .btn-navbar .fa-bar {
display: block;
width: 18px;
height: 2px;
@@ -3495,7 +3495,7 @@ input[type="submit"].btn.btn-mini {
-moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
}
-.btn-navbar .icon-bar + .icon-bar {
+.btn-navbar .fa-bar + .fa-bar {
margin-top: 3px;
}
.navbar .nav > li > .dropdown-menu:before {
@@ -6376,8 +6376,8 @@ textarea {
.form-horizontal .info-inline:before {
top: 8px;
}
-.info-block .icon-large,
-.info-inline .icon-large {
+.info-block .fa-lg,
+.info-inline .fa-lg {
float: left;
font-size: 22px;
margin-right: 15px;
@@ -6912,7 +6912,7 @@ textarea {
-moz-border-radius: 100px;
border-radius: 100px;
}
-.js .image-upload .btn-remove-url .icon-remove {
+.js .image-upload .btn-remove-url .fa-times {
margin-right: 0;
}
.add-member-form .control-label {
@@ -6975,7 +6975,7 @@ textarea {
margin-right: 10px;
text-transform: uppercase;
}
-.dataset-private .icon-lock {
+.dataset-private .fa-lock {
width: 9px;
}
.dataset-private.pull-right {
@@ -7908,39 +7908,39 @@ h4 small {
height: 35px;
background-position: -320px -62px;
}
-[class^="icon-"],
-[class*=" icon-"] {
+[class^="fa fa-"],
+[class*=" fa fa-"] {
display: inline-block;
text-align: right;
font-size: 14px;
line-height: 1;
width: 14px;
}
-.btn [class^="icon-"],
-.nav [class^="icon-"],
-.module-heading [class^="icon-"],
-.dropdown [class^="icon-"],
-.btn [class*=" icon-"],
-.nav [class*=" icon-"],
-.module-heading [class*=" icon-"],
-.dropdown [class*=" icon-"] {
+.btn [class^="fa fa-"],
+.nav [class^="fa fa-"],
+.module-heading [class^="fa fa-"],
+.dropdown [class^="fa fa-"],
+.btn [class*=" fa fa-"],
+.nav [class*=" fa fa-"],
+.module-heading [class*=" fa fa-"],
+.dropdown [class*=" fa fa-"] {
margin-right: 4px;
}
-.info-block [class^="icon-"],
-.info-block [class*=" icon-"] {
+.info-block [class^="fa fa-"],
+.info-block [class*=" fa fa-"] {
float: left;
font-size: 28px;
width: 28px;
margin-right: 5px;
margin-top: 2px;
}
-.breadcrumb .home .icon-home {
+.breadcrumb .home .fa-home {
font-size: 24px;
width: 24px;
vertical-align: -1px;
}
-.info-block-small [class^="icon-"],
-.info-block-small [class*=" icon-"] {
+.info-block-small [class^="fa fa-"],
+.info-block-small [class*=" fa fa-"] {
font-size: 14px;
width: 14px;
margin-top: 1px;
@@ -8522,13 +8522,13 @@ h4 small {
text-shadow: none;
margin-top: 15px;
}
-.masthead .btn-navbar .icon-bar,
-.masthead .btn-navbar:hover .icon-bar,
-.masthead .btn-navbar:focus .icon-bar,
-.masthead .btn-navbar:active .icon-bar,
-.masthead .btn-navbar.active .icon-bar,
-.masthead .btn-navbar.disabled .icon-bar,
-.masthead .btn-navbar[disabled] .icon-bar {
+.masthead .btn-navbar .fa-bar,
+.masthead .btn-navbar:hover .fa-bar,
+.masthead .btn-navbar:focus .fa-bar,
+.masthead .btn-navbar:active .fa-bar,
+.masthead .btn-navbar.active .fa-bar,
+.masthead .btn-navbar.disabled .fa-bar,
+.masthead .btn-navbar[disabled] .fa-bar {
margin-right: 0;
}
.masthead .debug {
@@ -8659,13 +8659,13 @@ h4 small {
text-shadow: none;
margin-top: 15px;
}
-.site-footer .btn-navbar .icon-bar,
-.site-footer .btn-navbar:hover .icon-bar,
-.site-footer .btn-navbar:focus .icon-bar,
-.site-footer .btn-navbar:active .icon-bar,
-.site-footer .btn-navbar.active .icon-bar,
-.site-footer .btn-navbar.disabled .icon-bar,
-.site-footer .btn-navbar[disabled] .icon-bar {
+.site-footer .btn-navbar .fa-bar,
+.site-footer .btn-navbar:hover .fa-bar,
+.site-footer .btn-navbar:focus .fa-bar,
+.site-footer .btn-navbar:active .fa-bar,
+.site-footer .btn-navbar.active .fa-bar,
+.site-footer .btn-navbar.disabled .fa-bar,
+.site-footer .btn-navbar[disabled] .fa-bar {
margin-right: 0;
}
.site-footer .debug {
@@ -9313,197 +9313,25 @@ iframe {
white-space: nowrap;
}
.ie9 .homepage .media.module-heading .media-image img,
-.ie8 .homepage .media.module-heading .media-image img,
-.ie7 .homepage .media.module-heading .media-image img {
+.ie8 .homepage .media.module-heading .media-image img {
width: 85px !important;
}
-.ie8 .masthead .nav-collapse,
-.ie7 .masthead .nav-collapse {
+.ie8 .masthead .nav-collapse {
float: right;
}
.ie8 [role=main],
-.ie7 [role=main],
-.ie8 .main,
-.ie7 .main {
+.ie8 .main {
padding-top: 10px;
background: #eeeeee url("../../../base/images/bg.png");
}
-.ie8 .hero,
-.ie7 .hero {
+.ie8 .hero {
background: url("../../../base/images/background-tile.png");
}
-.ie8 .hero .hero-primary.module-popup .box,
-.ie7 .hero .hero-primary.module-popup .box {
+.ie8 .hero .hero-primary.module-popup .box {
padding-bottom: 20px !important;
margin-bottom: 0 !important;
}
-.ie8 .lang-dropdown,
-.ie7 .lang-dropdown {
+.ie8 .lang-dropdown {
position: relative !important;
top: -90px !important;
}
-.ie7 .alert {
- position: relative;
-}
-.ie7 .alert .close {
- position: absolute;
- top: 6px !important;
- right: 20px;
-}
-.ie7 .media-item {
- width: 30%;
-}
-.ie7 .tags .tag-list {
- *zoom: 1;
-}
-.ie7 .tags .tag-list:before,
-.ie7 .tags .tag-list:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .tags .tag-list:after {
- clear: both;
-}
-.ie7 .tags .tag-list li {
- display: block;
- float: left;
-}
-.ie7 .tags h3 {
- float: left;
-}
-.ie7 .tags .tag {
- display: block;
-}
-.ie7 .search-giant input {
- width: 95%;
-}
-.ie7 .control-full input,
-.ie7 .control-full select,
-.ie7 .control-full textarea {
- width: 95%;
-}
-.ie7 .control-full.control-large .controls input {
- padding-bottom: 20px;
-}
-.ie7 .controls {
- position: relative;
-}
-.ie7 .controls .info-block,
-.ie7 .controls .info-inline {
- position: absolute;
- top: 0;
- right: 0;
-}
-.ie7 .form-horizontal .controls {
- margin-left: 0;
-}
-.ie7 .control-custom .checkbox {
- *display: inline;
- /* IE7 inline-block hack */
- *zoom: 1;
-}
-.ie7 .stages {
- overflow: hidden;
- background-color: #ededed;
-}
-.ie7 .stages li {
- height: 30px;
- width: 27.5%;
-}
-.ie7 .stages li button,
-.ie7 .stages li span {
- display: block;
- height: 30px;
- padding-left: 20px;
-}
-.ie7 .stages li button {
- height: 50px;
-}
-.ie7 .stages li .highlight {
- width: auto;
-}
-.ie7 .account-masthead .account a i {
- line-height: 31px;
-}
-.ie7 .masthead {
- position: relative;
- z-index: 1;
-}
-.ie7 .masthead .logo img,
-.ie7 .masthead nav {
- *display: inline;
- /* IE7 inline-block hack */
- *zoom: 1;
-}
-.ie7 .masthead .header-image {
- display: block;
-}
-.ie7 .masthead .account .dropdown-menu {
- z-index: 10000;
-}
-.ie7 .module-narrow .nav-item.image {
- *zoom: 1;
-}
-.ie7 .module-narrow .nav-item.image:before,
-.ie7 .module-narrow .nav-item.image:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .module-narrow .nav-item.image:after {
- clear: both;
-}
-.ie7 .nav-facet .nav-item.active a {
- content: 'x';
-}
-.ie7 .toolbar .breadcrumb li {
- padding-right: 10px;
- margin-right: 5px;
- background: transparent url("../../../base/images/breadcrumb-slash-ie7.png") 100% 50% no-repeat;
-}
-.ie7 .toolbar .breadcrumb li.active {
- background-image: none;
-}
-.ie7 .module-heading {
- *zoom: 1;
- position: relative;
-}
-.ie7 .module-heading:before,
-.ie7 .module-heading:after {
- display: table;
- content: "";
- line-height: 0;
-}
-.ie7 .module-heading:after {
- clear: both;
-}
-.ie7 .module-heading .media-content {
- position: relative;
-}
-.ie7 .module-heading .media-image img {
- float: left;
-}
-.ie7 .group-listing {
- position: relative;
- zoom: 1;
-}
-.ie7 .resource-item {
- position: static;
- padding-bottom: 1px;
-}
-.ie7 .resource-item .heading {
- position: relative;
-}
-.ie7 .resource-item .format-label {
- left: -48px;
-}
-.ie7 .resource-item .btn-group {
- position: relative;
- float: right;
- top: -35px;
- right: 0;
-}
-.ie7 .media-overlay .media-heading {
- background-color: #000;
-}
diff --git a/ckan/public/base/javascript/client.js b/ckan/public/base/javascript/client.js
index d2e3232b912..9dd75ea522f 100644
--- a/ckan/public/base/javascript/client.js
+++ b/ckan/public/base/javascript/client.js
@@ -309,7 +309,6 @@
* Returns an object of dataset keys.
*/
convertStorageMetadataToResource: function (meta) {
- // TODO: Check this is supported by IE7. U believe that the IE
// Date constructor chokes on hyphens and timezones.
var modified = new Date(this.normalizeTimestamp(meta._last_modified));
var created = new Date(this.normalizeTimestamp(meta._creation_date));
diff --git a/ckan/public/base/javascript/main.js b/ckan/public/base/javascript/main.js
index 1df0c2802cf..27ba0e22f86 100644
--- a/ckan/public/base/javascript/main.js
+++ b/ckan/public/base/javascript/main.js
@@ -91,16 +91,6 @@ this.ckan = this.ckan || {};
})(this.ckan, this.jQuery);
-// Forces this to redraw in Internet Explorer 7
-// This is useful for when IE7 doesn't properly render parts of the page after
-// some dom manipulation has happened
-this.jQuery.fn.ie7redraw = function() {
- if (jQuery('html').hasClass('ie7')) {
- jQuery(this).css('zoom', 1);
- }
-};
-
-
// Show / hide filters for mobile
$(function() {
$(".show-filters").click(function() {
diff --git a/ckan/public/base/javascript/modules/basic-form.js b/ckan/public/base/javascript/modules/basic-form.js
index dfd498a2a29..c1ef2da923f 100644
--- a/ckan/public/base/javascript/modules/basic-form.js
+++ b/ckan/public/base/javascript/modules/basic-form.js
@@ -3,16 +3,6 @@ this.ckan.module('basic-form', function (jQuery, _) {
initialize: function () {
var message = _('There are unsaved modifications to this form').fetch();
this.el.incompleteFormWarning(message);
- // Internet Explorer 7 fix for forms with