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
- User + User