diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 21a87c73948..3a117515ddf 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,6 +1,29 @@
CKAN CHANGELOG
++++++++++++++
+v1.8
+====
+
+* [#2592,#2428] Ubuntu 12.04 Precise is now supported with CKAN source install.
+ The source install instructions have been updated and simplified.
+ Some of CKAN's dependencies have been updated and some removed.
+* Requirements have been updated see doc/install-from-source.rst
+ users will need to do a new pip install (#2592)
+* [#2304] New 'follow' feature. You'll now see a 'Followers' tab on user and
+ dataset pages, where you can see how many users are following that user or
+ dataset. If you're logged in, you'll see a 'Follow' button on the pages of
+ datasets and other users that you can click to follow them. There are also
+ API calls for the follow features, see the Action API reference
+ documentation.
+* [#2305] New user dashboards, implemented by Sven R. Kunze
+ (https://github.com/kunsv) as part of his Masters thesis. When logged in, if
+ you go to your own user page you'll see a new 'Dashboard' tab where you can
+ see an activity stream from of all the users and datasets that you're
+ following.
+* [#2345] New action API reference docs. The documentation for CKAN's Action
+ API has been rewritten, with each function and its arguments and return
+ values now individually documented.
+
v1.7.1 2012-06-20
=================
diff --git a/ckan/config/deployment.ini_tmpl b/ckan/config/deployment.ini_tmpl
index d3c0199687f..d66d6543248 100644
--- a/ckan/config/deployment.ini_tmpl
+++ b/ckan/config/deployment.ini_tmpl
@@ -24,7 +24,7 @@ app_instance_uuid = ${app_instance_uuid}
# List the names of CKAN extensions to activate.
# Note: This line is required to be here for packaging, even if it is empty.
-ckan.plugins = stats synchronous_search
+ckan.plugins = stats
# If you'd like to fine-tune the individual locations of the cache data dirs
# for the Cache data, or the Session saves, un-comment the desired settings
@@ -112,6 +112,11 @@ ckan.gravatar_default = identicon
## Solr support
#solr_url = http://127.0.0.1:8983/solr
+## Automatic indexing. Make all changes immediately available via the search
+## after editing or creating a dataset. Default is true. If for some reason
+## you need the indexing to occur asynchronously, set this option to 0.
+# ckan.search.automatic_indexing = 1
+
## An 'id' for the site (using, for example, when creating entries in a common search index)
## If not specified derived from the site_url
# ckan.site_id = ckan.net
diff --git a/ckan/config/environment.py b/ckan/config/environment.py
index 350ca6f0599..e93d4df5605 100644
--- a/ckan/config/environment.py
+++ b/ckan/config/environment.py
@@ -21,12 +21,14 @@
import ckan.lib.search as search
import ckan.lib.app_globals as app_globals
+log = logging.getLogger(__name__)
import lib.jinja_tags
# Suppress benign warning 'Unbuilt egg for setuptools'
warnings.simplefilter('ignore', UserWarning)
+
class _Helpers(object):
''' Helper object giving access to template helpers stopping
missing functions from causing template exceptions. Useful if
@@ -97,13 +99,16 @@ def load_environment(global_conf, app_conf):
from pylons.wsgiapp import PylonsApp
import pkg_resources
find_controller_generic = PylonsApp.find_controller
+
# This is from pylons 1.0 source, will monkey-patch into 0.9.7
def find_controller(self, controller):
if controller in self.controller_classes:
return self.controller_classes[controller]
# Check to see if its a dotted name
if '.' in controller or ':' in controller:
- mycontroller = pkg_resources.EntryPoint.parse('x=%s' % controller).load(False)
+ mycontroller = pkg_resources \
+ .EntryPoint \
+ .parse('x=%s' % controller).load(False)
self.controller_classes[controller] = mycontroller
return mycontroller
return find_controller_generic(self, controller)
@@ -126,6 +131,13 @@ def find_controller(self, controller):
# load all CKAN plugins
p.load_all(config)
+ # Load the synchronous search plugin, unless already loaded or
+ # explicitly disabled
+ if not 'synchronous_search' in config.get('ckan.plugins') and \
+ asbool(config.get('ckan.search.automatic_indexing', True)):
+ log.debug('Loading the synchronous search plugin')
+ p.load('synchronous_search')
+
for plugin in p.PluginImplementations(p.IConfigurer):
# must do update in place as this does not work:
# config = plugin.update_config(config)
@@ -160,11 +172,13 @@ def find_controller(self, controller):
config['pylons.app_globals'] = app_globals.Globals()
# add helper functions
- restrict_helpers = asbool(config.get('ckan.restrict_template_vars', 'true'))
+ restrict_helpers = asbool(
+ config.get('ckan.restrict_template_vars', 'true'))
helpers = _Helpers(h, restrict_helpers)
config['pylons.h'] = helpers
- ## redo template setup to use genshi.search_path (so remove std template setup)
+ ## redo template setup to use genshi.search_path
+ ## (so remove std template setup)
legacy_templates_path = os.path.join(root, 'templates_legacy')
if asbool(config.get('ckan.legacy_templates', 'no')):
template_paths = [legacy_templates_path]
@@ -180,6 +194,7 @@ def find_controller(self, controller):
# Translator (i18n)
translator = Translator(pylons.translator)
+
def template_loaded(template):
translator.setup(template)
@@ -210,8 +225,6 @@ def template_loaded(template):
# #
#################################################################
-
-
'''
This code is based on Genshi code
@@ -297,11 +310,14 @@ def genshi_lookup_attr(cls, obj, key):
# Setup the SQLAlchemy database engine
# Suppress a couple of sqlalchemy warnings
- warnings.filterwarnings('ignore', '^Unicode type received non-unicode bind param value', sqlalchemy.exc.SAWarning)
- warnings.filterwarnings('ignore', "^Did not recognize type 'BIGINT' of column 'size'", sqlalchemy.exc.SAWarning)
- warnings.filterwarnings('ignore', "^Did not recognize type 'tsvector' of column 'search_vector'", sqlalchemy.exc.SAWarning)
+ msgs = ['^Unicode type received non-unicode bind param value',
+ "^Did not recognize type 'BIGINT' of column 'size'",
+ "^Did not recognize type 'tsvector' of column 'search_vector'"
+ ]
+ for msg in msgs:
+ warnings.filterwarnings('ignore', msg, sqlalchemy.exc.SAWarning)
- ckan_db = os.environ.get('CKAN_DB')
+ ckan_db = os.environ.get('CKAN_DB')
if ckan_db:
config['sqlalchemy.url'] = ckan_db
@@ -320,4 +336,3 @@ def genshi_lookup_attr(cls, obj, key):
for plugin in p.PluginImplementations(p.IConfigurable):
plugin.configure(config)
-
diff --git a/ckan/config/middleware.py b/ckan/config/middleware.py
index a6f2be3bbb6..67ddf437beb 100644
--- a/ckan/config/middleware.py
+++ b/ckan/config/middleware.py
@@ -21,7 +21,7 @@
from ckan.plugins import PluginImplementations
from ckan.plugins.interfaces import IMiddleware
-from ckan.lib.i18n import get_locales
+from ckan.lib.i18n import get_locales_from_config
from ckan.config.environment import load_environment
@@ -167,7 +167,7 @@ class I18nMiddleware(object):
def __init__(self, app, config):
self.app = app
self.default_locale = config.get('ckan.locale_default', 'en')
- self.local_list = get_locales()
+ self.local_list = get_locales_from_config()
def __call__(self, environ, start_response):
# strip the language selector from the requested url
diff --git a/ckan/config/routing.py b/ckan/config/routing.py
index 0a8365c1707..f03b10a204e 100644
--- a/ckan/config/routing.py
+++ b/ckan/config/routing.py
@@ -256,6 +256,7 @@ def make_map():
m.connect('/user/edit', action='edit')
# Note: openid users have slashes in their ids, so need the wildcard
# in the route.
+ m.connect('/user/dashboard', action='dashboard')
m.connect('/user/followers/{id:.*}', action='followers')
m.connect('/user/edit/{id:.*}', action='edit')
m.connect('/user/reset/{id:.*}', action='perform_reset')
diff --git a/ckan/config/solr/CHANGELOG.txt b/ckan/config/solr/CHANGELOG.txt
index 1e4e67f6ff6..eb600e042cc 100644
--- a/ckan/config/solr/CHANGELOG.txt
+++ b/ckan/config/solr/CHANGELOG.txt
@@ -8,6 +8,8 @@ v1.4 - (ckan>=1.7)
* Add title_string so you can sort alphabetically on title.
* Fields related to analytics, access and view counts.
* Add data_dict field for the whole package_dict.
+* Add vocab_* dynamic field so it is possible to facet by vocabulary tags
+* Add copyField for text with source vocab_*
v1.3 - (ckan>=1.5.1)
--------------------
diff --git a/ckan/config/solr/schema-1.4.xml b/ckan/config/solr/schema-1.4.xml
index 0409e71b14b..d98b9c56f5c 100644
--- a/ckan/config/solr/schema-1.4.xml
+++ b/ckan/config/solr/schema-1.4.xml
@@ -153,6 +153,7 @@
+
@@ -165,6 +166,7 @@
+
diff --git a/ckan/controllers/api.py b/ckan/controllers/api.py
index 723d4bbf6cc..66754fdb1f2 100644
--- a/ckan/controllers/api.py
+++ b/ckan/controllers/api.py
@@ -251,6 +251,7 @@ def list(self, ver=None, register=None, subregister=None, id=None):
('dataset', 'activity'): 'package_activity_list',
('group', 'activity'): 'group_activity_list',
('user', 'activity'): 'user_activity_list',
+ ('user', 'dashboard_activity'): 'dashboard_activity_list',
('activity', 'details'): 'activity_detail_list',
}
diff --git a/ckan/controllers/datastore.py b/ckan/controllers/datastore.py
index f911f150b15..b8199c8f7ff 100644
--- a/ckan/controllers/datastore.py
+++ b/ckan/controllers/datastore.py
@@ -1,9 +1,9 @@
from ckan.lib.base import BaseController, abort, _, c, response, request, g
import ckan.model as model
-from ckan.lib.helpers import json
from ckan.lib.jsonp import jsonpify
from ckan.logic import get_action, check_access
-from ckan.logic import NotFound, NotAuthorized, ValidationError
+from ckan.logic import NotFound, NotAuthorized
+
class DatastoreController(BaseController):
@@ -21,8 +21,6 @@ def read(self, id, url=''):
try:
resource = get_action('resource_show')(context, {'id': id})
- if not resource.get('webstore_url', ''):
- return {'error': 'DataStore is disabled for this resource'}
self._make_redirect(id, url)
return ''
except NotFound:
@@ -35,13 +33,12 @@ def write(self, id, url):
context = {'model': model, 'session': model.Session,
'user': c.user or c.author}
try:
- resource = model.Resource.get(id)
- if not resource:
- abort(404, _('Resource not found'))
- if not resource.webstore_url:
- return {'error': 'DataStore is disabled for this resource'}
- context["resource"] = resource
check_access('resource_update', context, {'id': id})
+ resource_dict = get_action('resource_show')(context,{'id':id})
+ if not resource_dict['webstore_url']:
+ resource_dict['webstore_url'] = u'active'
+ get_action('resource_update')(context,resource_dict)
+
self._make_redirect(id, url)
return ''
except NotFound:
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index 0c894d33726..1e49ad9c9b7 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -744,8 +744,7 @@ def _save_new(self, context, package_type=None):
except DataError:
abort(400, _(u'Integrity Error'))
except SearchIndexError, e:
- abort(500, _(u'Unable to add package to search index.') +
- repr(e.args))
+ abort(500, _(u'Unable to add package to search index.'))
except ValidationError, e:
errors = e.error_dict
error_summary = e.error_summary
@@ -783,7 +782,7 @@ def _save_edit(self, name_or_id, context):
except DataError:
abort(400, _(u'Integrity Error'))
except SearchIndexError, e:
- abort(500, _(u'Unable to update search index.') + repr(e.args))
+ abort(500, _(u'Unable to update search index.'))
except ValidationError, e:
errors = e.error_dict
error_summary = e.error_summary
diff --git a/ckan/controllers/related.py b/ckan/controllers/related.py
index a6977ec83dd..9562dc6153c 100644
--- a/ckan/controllers/related.py
+++ b/ckan/controllers/related.py
@@ -42,7 +42,7 @@ def list(self, id):
base.abort(401, base._('Unauthorized to read package %s') % id)
c.related_count = len(c.pkg.related)
-
+ c.action = 'related'
return base.render("package/related_list.html")
def _edit_or_new(self, id, related_id, is_edit):
diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py
index 0b2d276d852..afb90adf2c8 100644
--- a/ckan/controllers/user.py
+++ b/ckan/controllers/user.py
@@ -5,6 +5,7 @@
from urllib import quote
import ckan.misc
+import ckan.lib.i18n
from ckan.lib.base import *
from ckan.lib import mailer
from ckan.authz import Authorizer
@@ -117,8 +118,8 @@ def me(self, locale=None):
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='read', id=user_ref)
+ h.redirect_to(locale=locale, controller='user', action='dashboard',
+ id=user_ref)
def register(self, data=None, errors=None, error_summary=None):
return self.new(data, errors, error_summary)
@@ -291,6 +292,11 @@ def logged_in(self):
lang = session.pop('lang', None)
session.save()
came_from = request.params.get('came_from', '')
+
+ # we need to set the language explicitly here or the flash
+ # messages will not be translated.
+ ckan.lib.i18n.set_lang(lang)
+
if c.user:
context = {'model': model,
'user': c.user}
@@ -451,3 +457,10 @@ def followers(self, id=None):
f = get_action('user_follower_list')
c.followers = f(context, {'id': c.user_dict['id']})
return render('user/followers.html')
+
+ def dashboard(self, id=None):
+ context = {'model': model, 'session': model.Session,
+ 'user': c.user or c.author, 'for_view': True}
+ data_dict = {'id': id, 'user_obj': c.userobj}
+ self._setup_template_variables(context, data_dict)
+ return render('user/dashboard.html')
diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py
index 899b564a6d6..3dc9048dee1 100644
--- a/ckan/lib/helpers.py
+++ b/ckan/lib/helpers.py
@@ -676,7 +676,7 @@ def dict_list_reduce(list_, key, unique=True):
def linked_gravatar(email_hash, size=100, default=None):
return literal(
- '' % _('Update your avatar at gravatar.com') +
'%s' % gravatar(email_hash,size,default)
)
@@ -1160,6 +1160,21 @@ def groups_available():
data_dict = {'available_only': True}
return logic.get_action('group_list_authz')(context, data_dict)
+def dashboard_activity_stream(user_id):
+ '''Return the dashboard activity stream of the given user.
+
+ :param user_id: the id of the user
+ :type user_id: string
+
+ :returns: an activity stream as an HTML snippet
+ :rtype: string
+
+ '''
+ import ckan.logic as logic
+ context = {'model' : model, 'session':model.Session, 'user':c.user}
+ return logic.get_action('dashboard_activity_list_html')(context, {'id': user_id})
+
+
# these are the functions that will end up in `h` template helpers
# if config option restrict_template_vars is true
__allowed_functions__ = [
@@ -1229,6 +1244,7 @@ def groups_available():
'remove_url_param',
'add_url_param',
'groups_available',
+ 'dashboard_activity_stream',
# imported into ckan.lib.helpers
'literal',
'link_to',
diff --git a/ckan/lib/i18n.py b/ckan/lib/i18n.py
index ca04a84cce3..ca6c1682418 100644
--- a/ckan/lib/i18n.py
+++ b/ckan/lib/i18n.py
@@ -12,14 +12,26 @@
# we don't have a Portuguese territory
# translation currently.
-def _get_locales():
+def get_locales_from_config():
+ ''' despite the name of this function it gets the locales defined by
+ the config AND also the locals available subject to the config. '''
+ locales_offered = config.get('ckan.locales_offered', '').split()
+ filtered_out = config.get('ckan.locales_filtered_out', '').split()
+ locale_default = config.get('ckan.locale_default', 'en')
+ locale_order = config.get('ckan.locale_order', '').split()
+ known_locales = get_locales()
+ all_locales = set(known_locales) | set(locales_offered) | set(locale_order) | set(locale_default)
+ all_locales -= set(filtered_out)
+ return all_locales
+def _get_locales():
+ # FIXME this wants cleaning up and merging with get_locales_from_config()
assert not config.get('lang'), \
'"lang" config option not supported - please use ckan.locale_default instead.'
locales_offered = config.get('ckan.locales_offered', '').split()
filtered_out = config.get('ckan.locales_filtered_out', '').split()
- locale_order = config.get('ckan.locale_order', '').split()
locale_default = config.get('ckan.locale_default', 'en')
+ locale_order = config.get('ckan.locale_order', '').split()
locales = ['en']
i18n_path = os.path.dirname(ckan.i18n.__file__)
@@ -58,6 +70,7 @@ def _get_locales():
available_locales = None
locales = None
locales_dict = None
+_non_translated_locals = None
def get_locales():
''' Get list of available locales
@@ -68,6 +81,15 @@ def get_locales():
locales = _get_locales()
return locales
+def non_translated_locals():
+ ''' These are the locales that are available but for which there are
+ no translations. returns a list like ['en', 'de', ...] '''
+ global _non_translated_locals
+ if not _non_translated_locals:
+ locales = config.get('ckan.locale_order', '').split()
+ _non_translated_locals = [x for x in locales if x not in get_locales()]
+ return _non_translated_locals
+
def get_locales_dict():
''' Get a dict of the available locales
e.g. { 'en' : Locale('en'), 'de' : Locale('de'), ... } '''
@@ -87,12 +109,25 @@ def get_available_locales():
available_locales = map(Locale.parse, get_locales())
return available_locales
+def _set_lang(lang):
+ ''' Allows a custom i18n directory to be specified.
+ Creates a fake config file to pass to pylons.i18n.set_lang, which
+ sets the Pylons root path to desired i18n_directory.
+ This is needed as Pylons will only look for an i18n directory in
+ the application root.'''
+ if config.get('ckan.i18n_directory'):
+ fake_config = {'pylons.paths': {'root': config['ckan.i18n_directory']},
+ 'pylons.package': config['pylons.package']}
+ i18n.set_lang(lang, pylons_config=fake_config)
+ else:
+ i18n.set_lang(lang)
+
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')
+ config.get('ckan.locale_default', 'en')
if lang != 'en':
- i18n.set_lang(lang)
+ set_lang(lang)
tmpl_context.language = lang
return lang
@@ -107,5 +142,7 @@ def get_lang():
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':
- i18n.set_lang(language_code)
+ _set_lang(language_code)
diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py
index 258e1449681..bb5b795aa18 100644
--- a/ckan/lib/plugins.py
+++ b/ckan/lib/plugins.py
@@ -221,11 +221,21 @@ def form_to_db_schema_options(self, options):
if options.get('api'):
if options.get('type') == 'create':
- return logic.schema.default_create_package_schema()
+ return self.form_to_db_schema_api_create()
else:
- return logic.schema.default_update_package_schema()
+ assert options.get('type') == 'update'
+ return self.form_to_db_schema_api_update()
else:
- return logic.schema.package_form_schema()
+ return self.form_to_db_schema()
+
+ def form_to_db_schema(self):
+ return logic.schema.form_to_db_package_schema()
+
+ def form_to_db_schema_api_create(self):
+ return logic.schema.default_create_package_schema()
+
+ def form_to_db_schema_api_update(self):
+ return logic.schema.default_update_package_schema()
def db_to_form_schema(self):
'''This is an interface to manipulate data from the database
@@ -387,7 +397,7 @@ def check_data_dict(self, data_dict):
'extras_validation', 'save', 'return_to',
'resources']
- schema_keys = package_form_schema().keys()
+ schema_keys = form_to_db_package_schema().keys()
keys_in_schema = set(schema_keys) - set(surplus_keys_schema)
missing_keys = keys_in_schema - set(data_dict.keys())
diff --git a/ckan/lib/search/index.py b/ckan/lib/search/index.py
index e9129009947..09ec2eec913 100644
--- a/ckan/lib/search/index.py
+++ b/ckan/lib/search/index.py
@@ -1,7 +1,6 @@
import socket
import string
import logging
-import itertools
import collections
import json
@@ -14,6 +13,7 @@
import ckan.model as model
from ckan.plugins import (PluginImplementations,
IPackageController)
+import ckan.logic as logic
log = logging.getLogger(__name__)
@@ -122,10 +122,27 @@ def index_package(self, pkg_dict):
pkg_dict[key] = value
pkg_dict.pop('extras', None)
- #Add tags and groups
+ # add tags, removing vocab tags from 'tags' list and adding them as
+ # vocab_ so that they can be used in facets
+ non_vocab_tag_names = []
tags = pkg_dict.pop('tags', [])
- pkg_dict['tags'] = [tag['name'] for tag in tags]
-
+ context = {'model': model}
+
+ 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']
+ if key in pkg_dict:
+ pkg_dict[key].append(tag['name'])
+ else:
+ pkg_dict[key] = [tag['name']]
+ else:
+ non_vocab_tag_names.append(tag['name'])
+
+ pkg_dict['tags'] = non_vocab_tag_names
+
+ # add groups
groups = pkg_dict.pop('groups', [])
# Capacity is different to the default only if using organizations
@@ -197,7 +214,6 @@ def index_package(self, pkg_dict):
import hashlib
pkg_dict['index_id'] = hashlib.md5('%s%s' % (pkg_dict['id'],config.get('ckan.site_id'))).hexdigest()
-
for item in PluginImplementations(IPackageController):
pkg_dict = item.before_index(pkg_dict)
diff --git a/ckan/lib/search/query.py b/ckan/lib/search/query.py
index a3b7adf35fe..25b9ba5bba7 100644
--- a/ckan/lib/search/query.py
+++ b/ckan/lib/search/query.py
@@ -150,16 +150,26 @@ def run(self, query=None, terms=[], fields={}, facet_by=[], options=None, **kwar
class TagSearchQuery(SearchQuery):
"""Search for tags."""
- def run(self, query=[], fields={}, options=None, **kwargs):
+ def run(self, query=None, fields=None, options=None, **kwargs):
+ query = [] if query is None else query
+ fields = {} if fields is None else fields
+
if options is None:
options = QueryOptions(**kwargs)
else:
options.update(kwargs)
+ if isinstance(query, basestring):
+ query = [query]
+
+ query = query[:] # don't alter caller's query list.
+ for field, value in fields.items():
+ if field in ('tag', 'tags'):
+ query.append(value)
+
context = {'model': model, 'session': model.Session}
data_dict = {
'query': query,
- 'fields': fields,
'offset': options.get('offset'),
'limit': options.get('limit')
}
@@ -186,9 +196,23 @@ def run(self, fields={}, options=None, **kwargs):
else:
options.update(kwargs)
- context = {'model':model, 'session': model.Session}
+ context = {
+ 'model':model,
+ 'session': model.Session,
+ 'search_query': True,
+ }
+
+ # Transform fields into structure required by the resource_search
+ # action.
+ query = []
+ for field, terms in fields.items():
+ if isinstance(terms, basestring):
+ terms = terms.split()
+ for term in terms:
+ query.append(':'.join([field, term]))
+
data_dict = {
- 'fields': fields,
+ 'query': query,
'offset': options.get('offset'),
'limit': options.get('limit'),
'order_by': options.get('order_by')
diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py
index 874abc88685..ce44abfa249 100644
--- a/ckan/logic/action/create.py
+++ b/ckan/logic/action/create.py
@@ -534,7 +534,8 @@ def group_create(context, data_dict):
'defer_commit':True,
'session': session
}
- activity_create(activity_create_context, activity_dict, ignore_auth=True)
+ logic.get_action('activity_create')(activity_create_context,
+ activity_dict, ignore_auth=True)
if not context.get('defer_commit'):
model.repo.commit()
@@ -648,7 +649,8 @@ def user_create(context, data_dict):
'object_id': user.id,
'activity_type': 'new user',
}
- activity_create(activity_create_context, activity_dict, ignore_auth=True)
+ logic.get_action('activity_create')(activity_create_context,
+ activity_dict, ignore_auth=True)
if not context.get('defer_commit'):
model.repo.commit()
@@ -842,6 +844,7 @@ def follow_user(context, data_dict):
raise logic.NotAuthorized
model = context['model']
+ session = context['session']
userobj = model.User.get(context['user'])
if not userobj:
@@ -869,6 +872,24 @@ def follow_user(context, data_dict):
follower = model_save.user_following_user_dict_save(data_dict, context)
+ activity_dict = {
+ 'user_id': userobj.id,
+ 'object_id': data_dict['id'],
+ 'activity_type': 'follow user',
+ }
+ activity_dict['data'] = {
+ 'user': ckan.lib.dictization.table_dictize(
+ model.User.get(data_dict['id']), context),
+ }
+ activity_create_context = {
+ 'model': model,
+ 'user': userobj,
+ 'defer_commit':True,
+ 'session': session
+ }
+ logic.get_action('activity_create')(activity_create_context,
+ activity_dict, ignore_auth=True)
+
if not context.get('defer_commit'):
model.repo.commit()
@@ -895,6 +916,7 @@ def follow_dataset(context, data_dict):
raise logic.NotAuthorized
model = context['model']
+ session = context['session']
userobj = model.User.get(context['user'])
if not userobj:
@@ -918,6 +940,24 @@ def follow_dataset(context, data_dict):
follower = model_save.user_following_dataset_dict_save(data_dict, context)
+ activity_dict = {
+ 'user_id': userobj.id,
+ 'object_id': data_dict['id'],
+ 'activity_type': 'follow dataset',
+ }
+ activity_dict['data'] = {
+ 'dataset': ckan.lib.dictization.table_dictize(
+ model.Package.get(data_dict['id']), context),
+ }
+ activity_create_context = {
+ 'model': model,
+ 'user': userobj,
+ 'defer_commit':True,
+ 'session': session
+ }
+ logic.get_action('activity_create')(activity_create_context,
+ activity_dict, ignore_auth=True)
+
if not context.get('defer_commit'):
model.repo.commit()
diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py
index 191a72f7fea..acdab602e75 100644
--- a/ckan/logic/action/get.py
+++ b/ckan/logic/action/get.py
@@ -1178,24 +1178,114 @@ def package_search(context, data_dict):
def resource_search(context, data_dict):
'''
+ Searches for resources satisfying a given search criteria.
- :param fields:
- :type fields:
- :param order_by:
- :type order_by:
- :param offset:
- :type offset:
- :param limit:
- :type limit:
+ It returns a dictionary with 2 fields: ``count`` and ``results``. The
+ ``count`` field contains the total number of Resources found without the
+ limit or query parameters having an effect. The ``results`` field is a
+ list of dictized Resource objects.
- :returns:
- :rtype:
+ The 'q' parameter is a required field. It is a string of the form
+ ``{field}:{term}`` or a list of strings, each of the same form. Within
+ each string, ``{field}`` is a field or extra field on the Resource domain
+ object.
+
+ If ``{field}`` is ``"hash"``, then an attempt is made to match the
+ `{term}` as a *prefix* of the ``Resource.hash`` field.
+
+ If ``{field}`` is an extra field, then an attempt is made to match against
+ the extra fields stored against the Resource.
+
+ Note: The search is limited to search against extra fields declared in
+ the config setting ``ckan.extra_resource_fields``.
+
+ Note: Due to a Resource's extra fields being stored as a json blob, the
+ match is made against the json string representation. As such, false
+ positives may occur:
+
+ If the search criteria is: ::
+
+ query = "field1:term1"
+
+ Then a json blob with the string representation of: ::
+
+ {"field1": "foo", "field2": "term1"}
+
+ will match the search criteria! This is a known short-coming of this
+ approach.
+
+ All matches are made ignoring case; and apart from the ``"hash"`` field,
+ a term matches if it is a substring of the field's value.
+
+ Finally, when specifying more than one search criteria, the criteria are
+ AND-ed together.
+
+ The ``order`` parameter is used to control the ordering of the results.
+ Currently only ordering one field is available, and in ascending order
+ only.
+
+ The ``fields`` parameter is deprecated as it is not compatible with calling
+ this action with a GET request to the action API.
+
+ The context may contain a flag, `search_query`, which if True will make
+ this action behave as if being used by the internal search api. ie - the
+ results will not be dictized, and SearchErrors are thrown for bad search
+ queries (rather than ValidationErrors).
+
+ :param query: The search criteria. See above for description.
+ :type query: string or list of strings of the form "{field}:{term1}"
+ :param fields: Deprecated
+ :type fields: dict of fields to search terms.
+ :param order_by: A field on the Resource model that orders the results.
+ :type order_by: string
+ :param offset: Apply an offset to the query.
+ :type offset: int
+ :param limit: Apply a limit to the query.
+ :type limit: int
+
+ :returns: A dictionary with a ``count`` field, and a ``results`` field.
+ :rtype: dict
'''
model = context['model']
- session = context['session']
- fields = _get_or_bust(data_dict, 'fields')
+ # Allow either the `query` or `fields` parameter to be given, but not both.
+ # Once `fields` parameter is dropped, this can be made simpler.
+ # The result of all this gumpf is to populate the local `fields` variable
+ # with mappings from field names to list of search terms, or a single
+ # search-term string.
+ query = data_dict.get('query')
+ fields = data_dict.get('fields')
+
+ if query is None and fields is None:
+ raise ValidationError({'query': _('Missing value')})
+
+ elif query is not None and fields is not None:
+ raise ValidationError(
+ {'fields': _('Do not specify if using "query" parameter')})
+
+ elif query is not None:
+ if isinstance(query, basestring):
+ query = [query]
+ try:
+ fields = dict(pair.split(":", 1) for pair in query)
+ except ValueError:
+ raise ValidationError(
+ {'query': _('Must be : pair(s)')})
+
+ else:
+ log.warning('Use of the "fields" parameter in resource_search is '
+ 'deprecated. Use the "query" parameter instead')
+
+ # The legacy fields paramter splits string terms.
+ # So maintain that behaviour
+ split_terms = {}
+ for field, terms in fields.items():
+ if isinstance(terms, basestring):
+ terms = terms.split()
+ split_terms[field] = terms
+ fields = split_terms
+
order_by = data_dict.get('order_by')
offset = data_dict.get('offset')
limit = data_dict.get('limit')
@@ -1203,16 +1293,36 @@ def resource_search(context, data_dict):
# TODO: should we check for user authentication first?
q = model.Session.query(model.Resource)
resource_fields = model.Resource.get_columns()
-
for field, terms in fields.items():
+
if isinstance(terms, basestring):
- terms = terms.split()
+ terms = [terms]
+
if field not in resource_fields:
- raise search.SearchError('Field "%s" not recognised in Resource search.' % field)
+ msg = _('Field "{field}" not recognised in resource_search.')\
+ .format(field=field)
+
+ # Running in the context of the internal search api.
+ if context.get('search_query', False):
+ raise search.SearchError(msg)
+
+ # Otherwise, assume we're in the context of an external api
+ # and need to provide meaningful external error messages.
+ raise ValidationError({'query': msg})
+
for term in terms:
+
+ # prevent pattern injection
+ term = misc.escape_sql_like_special_characters(term)
+
model_attr = getattr(model.Resource, field)
+
+ # Treat the has field separately, see docstring.
if field == 'hash':
q = q.filter(model_attr.ilike(unicode(term) + '%'))
+
+ # Resource extras are stored in a json blob. So searching for
+ # matching fields is a bit trickier. See the docstring.
elif field in model.Resource.get_extra_columns():
model_attr = getattr(model.Resource, 'extras')
@@ -1221,6 +1331,8 @@ def resource_search(context, data_dict):
model_attr.ilike(u'''%%"%s": "%%%s%%"}''' % (field, term))
)
q = q.filter(like)
+
+ # Just a regular field
else:
q = q.filter(model_attr.ilike('%' + unicode(term) + '%'))
@@ -1240,15 +1352,24 @@ def resource_search(context, data_dict):
else:
results.append(result)
- return {'count': count, 'results': results}
+ # If run in the context of a search query, then don't dictize the results.
+ if not context.get('search_query', False):
+ results = model_dictize.resource_list_dictize(results, context)
+
+ return {'count': count,
+ 'results': results}
def _tag_search(context, data_dict):
model = context['model']
- query = data_dict.get('query') or data_dict.get('q')
- if query:
- query = query.strip()
- terms = [query] if query else []
+ terms = data_dict.get('query') or data_dict.get('q') or []
+ if isinstance(terms, basestring):
+ terms = [terms]
+ terms = [ t.strip() for t in terms if t.strip() ]
+
+ if 'fields' in data_dict:
+ log.warning('"fields" parameter is deprecated. '
+ 'Use the "query" parameter instead')
fields = data_dict.get('fields', {})
offset = data_dict.get('offset')
@@ -1293,12 +1414,12 @@ def tag_search(context, data_dict):
searched. If the ``vocabulary_id`` argument is given then only tags
belonging to that vocabulary will be searched instead.
- :param query: the string to search for
- :type query: string
+ :param query: the string(s) to search for
+ :type query: string or list of strings
:param vocabulary_id: the id or name of the tag vocabulary to search in
(optional)
:type vocabulary_id: string
- :param fields:
+ :param fields: deprecated
:type fields: dictionary
:param limit: the maximum number of tags to return
:type limit: int
@@ -1334,7 +1455,7 @@ def tag_autocomplete(context, data_dict):
:param vocabulary_id: the id or name of the tag vocabulary to search in
(optional)
:type vocabulary_id: string
- :param fields:
+ :param fields: deprecated
:type fields: dictionary
:param limit: the maximum number of tags to return
:type limit: int
@@ -1746,6 +1867,14 @@ def _render_deleted_group_activity(context, activity):
return _render('activity_streams/deleted_group.html',
extra_vars = {'activity': activity})
+def _render_follow_dataset_activity(context, activity):
+ return _render('activity_streams/follow_dataset.html',
+ extra_vars = {'activity': activity})
+
+def _render_follow_user_activity(context, activity):
+ return _render('activity_streams/follow_user.html',
+ extra_vars = {'activity': activity})
+
# Global dictionary mapping activity types to functions that render activity
# dicts to HTML snippets for including in HTML pages.
activity_renderers = {
@@ -1757,6 +1886,8 @@ def _render_deleted_group_activity(context, activity):
'new group' : _render_new_group_activity,
'changed group' : _render_changed_group_activity,
'deleted group' : _render_deleted_group_activity,
+ 'follow dataset': _render_follow_dataset_activity,
+ 'follow user': _render_follow_user_activity,
}
def _activity_list_to_html(context, activity_stream):
@@ -1834,6 +1965,7 @@ def user_follower_count(context, data_dict):
:param id: the id or name of the user
:type id: string
+
:rtype: int
'''
@@ -1849,6 +1981,7 @@ def dataset_follower_count(context, data_dict):
:param id: the id or name of the dataset
:type id: string
+
:rtype: int
'''
@@ -1869,7 +2002,7 @@ def _follower_list(context, data_dict, FollowerClass):
users = [model.User.get(follower.follower_id) for follower in followers]
users = [user for user in users if user is not None]
- # Dictize the list of user objects.
+ # Dictize the list of User objects.
return [model_dictize.user_dictize(user,context) for user in users]
def user_follower_list(context, data_dict):
@@ -1877,6 +2010,7 @@ def user_follower_list(context, data_dict):
:param id: the id or name of the user
:type id: string
+
:rtype: list of dictionaries
'''
@@ -1893,6 +2027,7 @@ def dataset_follower_list(context, data_dict):
:param id: the id or name of the dataset
:type id: string
+
:rtype: list of dictionaries
'''
@@ -1923,6 +2058,7 @@ def am_following_user(context, data_dict):
:param id: the id or name of the user
:type id: string
+
:rtype: boolean
'''
@@ -1940,6 +2076,7 @@ def am_following_dataset(context, data_dict):
:param id: the id or name of the dataset
:type id: string
+
:rtype: boolean
'''
@@ -1951,3 +2088,133 @@ def am_following_dataset(context, data_dict):
return _am_following(context, data_dict,
context['model'].UserFollowingDataset)
+
+def user_followee_count(context, data_dict):
+ '''Return the number of users that are followed by the given user.
+
+ :param id: the id of the user
+ :type id: string
+
+ :rtype: int
+
+ '''
+ schema = context.get('schema') or (
+ ckan.logic.schema.default_follow_user_schema())
+ data_dict, errors = _validate(data_dict, schema, context)
+ if errors:
+ raise ValidationError(errors, ckan.logic.action.error_summary(errors))
+ return ckan.model.UserFollowingUser.followee_count(data_dict['id'])
+
+def dataset_followee_count(context, data_dict):
+ '''Return the number of datasets that are followed by the given user.
+
+ :param id: the id of the user
+ :type id: string
+
+ :rtype: int
+
+ '''
+ schema = context.get('schema') or (
+ ckan.logic.schema.default_follow_user_schema())
+ data_dict, errors = _validate(data_dict, schema, context)
+ if errors:
+ raise ValidationError(errors, ckan.logic.action.error_summary(errors))
+ return ckan.model.UserFollowingDataset.followee_count(data_dict['id'])
+
+def user_followee_list(context, data_dict):
+ '''Return the list of users that are followed by the given user.
+
+ :param id: the id of the user
+ :type id: string
+
+ :rtype: list of dictionaries
+
+ '''
+ schema = context.get('schema') or (
+ ckan.logic.schema.default_follow_user_schema())
+ data_dict, errors = _validate(data_dict, schema, context)
+ if errors:
+ raise ValidationError(errors, ckan.logic.action.error_summary(errors))
+
+ # Get the list of Follower objects.
+ model = context['model']
+ user_id = data_dict.get('id')
+ followees = model.UserFollowingUser.followee_list(user_id)
+
+ # Convert the list of Follower objects to a list of User objects.
+ users = [model.User.get(followee.object_id) for followee in followees]
+ users = [user for user in users if user is not None]
+
+ # Dictize the list of User objects.
+ return [model_dictize.user_dictize(user, context) for user in users]
+
+def dataset_followee_list(context, data_dict):
+ '''Return the list of datasets that are followed by the given user.
+
+ :param id: the id or name of the user
+ :type id: string
+
+ :rtype: list of dictionaries
+
+ '''
+ schema = context.get('schema') or (
+ ckan.logic.schema.default_follow_user_schema())
+ data_dict, errors = _validate(data_dict, schema, context)
+ if errors:
+ raise ValidationError(errors, ckan.logic.action.error_summary(errors))
+
+ # Get the list of Follower objects.
+ model = context['model']
+ user_id = data_dict.get('id')
+ followees = model.UserFollowingDataset.followee_list(user_id)
+
+ # Convert the list of Follower objects to a list of Package objects.
+ datasets = [model.Package.get(followee.object_id) for followee in followees]
+ datasets = [dataset for dataset in datasets if dataset is not None]
+
+ # Dictize the list of Package objects.
+ return [model_dictize.package_dictize(dataset, context) for dataset in datasets]
+
+def dashboard_activity_list(context, data_dict):
+ '''Return the dashboard activity stream of the given user.
+
+ :param id: the id or name of the user
+ :type id: string
+
+ :rtype: list of dictionaries
+
+ '''
+ model = context['model']
+ user_id = _get_or_bust(data_dict, 'id')
+
+ activity_query = model.Session.query(model.Activity)
+ user_followees_query = activity_query.join(model.UserFollowingUser, model.UserFollowingUser.object_id == model.Activity.user_id)
+ dataset_followees_query = activity_query.join(model.UserFollowingDataset, model.UserFollowingDataset.object_id == model.Activity.object_id)
+
+ from_user_query = activity_query.filter(model.Activity.user_id==user_id)
+ about_user_query = activity_query.filter(model.Activity.object_id==user_id)
+ user_followees_query = user_followees_query.filter(model.UserFollowingUser.follower_id==user_id)
+ dataset_followees_query = dataset_followees_query.filter(model.UserFollowingDataset.follower_id==user_id)
+
+ query = from_user_query.union(about_user_query).union(
+ user_followees_query).union(dataset_followees_query)
+ query = query.order_by(_desc(model.Activity.timestamp))
+ query = query.limit(15)
+ activity_objects = query.all()
+
+ return model_dictize.activity_list_dictize(activity_objects, context)
+
+def dashboard_activity_list_html(context, data_dict):
+ '''Return the dashboard activity stream of the given user 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
+
+ :rtype: string
+
+ '''
+ activity_stream = dashboard_activity_list(context, data_dict)
+ return _activity_list_to_html(context, activity_stream)
diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py
index f36960b4db0..6bdb48d2cc2 100644
--- a/ckan/logic/auth/publisher/create.py
+++ b/ckan/logic/auth/publisher/create.py
@@ -12,18 +12,20 @@
def package_create(context, data_dict=None):
model = context['model']
user = context['user']
- userobj = model.User.get( user )
+ userobj = model.User.get(user)
- if userobj:
+ if userobj and len(userobj.get_groups()):
return {'success': True}
- return {'success': False, 'msg': 'You must be logged in to create a package'}
+ return {'success': False,
+ 'msg': _('You must be logged in and be within a group to create '
+ 'a package')}
def related_create(context, data_dict=None):
model = context['model']
user = context['user']
- userobj = model.User.get( user )
+ userobj = model.User.get(user)
if userobj:
return {'success': True}
diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
index aa194429034..f798b239ac8 100644
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -153,6 +153,11 @@ def default_update_package_schema():
return schema
def package_form_schema():
+ # This function is deprecated and was replaced by
+ # form_to_db_package_schema(), it remains here for backwards compatibility.
+ return form_to_db_package_schema()
+
+def form_to_db_package_schema():
schema = default_package_schema()
##new
@@ -175,6 +180,26 @@ def package_form_schema():
schema.pop('relationships_as_subject')
return schema
+def db_to_form_package_schema():
+ schema = default_package_schema()
+ # Workaround a bug in CKAN's convert_from_tags() function.
+ # TODO: Fix this issue in convert_from_tags().
+ schema.update({
+ 'tags': {
+ '__extras': [ckan.lib.navl.validators.keep_extras,
+ ckan.logic.converters.free_tags_only]
+ },
+ })
+ # Workaround a bug in CKAN.
+ # TODO: Fix this elsewhere so we don't need to workaround it here.
+ schema['resources'].update({
+ 'created': [ckan.lib.navl.validators.ignore_missing],
+ 'last_modified': [ckan.lib.navl.validators.ignore_missing],
+ 'cache_last_updated': [ckan.lib.navl.validators.ignore_missing],
+ 'webstore_last_updated': [ckan.lib.navl.validators.ignore_missing],
+ })
+ return schema
+
def default_group_schema():
schema = {
diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py
index 9250c75b5df..9030328df63 100644
--- a/ckan/logic/validators.py
+++ b/ckan/logic/validators.py
@@ -152,8 +152,10 @@ def activity_type_exists(activity_type):
'new package' : package_id_exists,
'changed package' : package_id_exists,
'deleted package' : package_id_exists,
+ 'follow dataset' : package_id_exists,
'new user' : user_id_exists,
'changed user' : user_id_exists,
+ 'follow user' : user_id_exists,
'new group' : group_id_exists,
'changed group' : group_id_exists,
'deleted group' : group_id_exists,
diff --git a/ckan/model/follower.py b/ckan/model/follower.py
index 0698867682c..0b3240ac9b7 100644
--- a/ckan/model/follower.py
+++ b/ckan/model/follower.py
@@ -27,24 +27,39 @@ def get(self, follower_id, object_id):
return query.first()
@classmethod
- def follower_count(cls, object_id):
- '''Return the number of users following a user.'''
+ def is_following(cls, follower_id, object_id):
+ '''Return True if follower_id is currently following object_id, False
+ otherwise.
+
+ '''
+ return UserFollowingUser.get(follower_id, object_id) is not None
+
+
+ @classmethod
+ def followee_count(cls, follower_id):
+ '''Return the number of users followed by a user.'''
return meta.Session.query(UserFollowingUser).filter(
- UserFollowingUser.object_id == object_id).count()
+ UserFollowingUser.follower_id == follower_id).count()
@classmethod
- def follower_list(cls, object_id):
- '''Return a list of all of the followers of a user.'''
+ def followee_list(cls, follower_id):
+ '''Return a list of users followed by a user.'''
return meta.Session.query(UserFollowingUser).filter(
- UserFollowingUser.object_id == object_id).all()
+ UserFollowingUser.follower_id == follower_id).all()
+
@classmethod
- def is_following(cls, follower_id, object_id):
- '''Return True if follower_id is currently following object_id, False
- otherwise.
+ def follower_count(cls, user_id):
+ '''Return the number of followers of a user.'''
+ return meta.Session.query(UserFollowingUser).filter(
+ UserFollowingUser.object_id == user_id).count()
+
+ @classmethod
+ def follower_list(cls, user_id):
+ '''Return a list of followers of a user.'''
+ return meta.Session.query(UserFollowingUser).filter(
+ UserFollowingUser.object_id == user_id).all()
- '''
- return UserFollowingUser.get(follower_id, object_id) is not None
user_following_user_table = sqlalchemy.Table('user_following_user',
meta.metadata,
@@ -85,24 +100,39 @@ def get(self, follower_id, object_id):
return query.first()
@classmethod
- def follower_count(cls, object_id):
- '''Return the number of users following a dataset.'''
+ def is_following(cls, follower_id, object_id):
+ '''Return True if follower_id is currently following object_id, False
+ otherwise.
+
+ '''
+ return UserFollowingDataset.get(follower_id, object_id) is not None
+
+
+ @classmethod
+ def followee_count(cls, follower_id):
+ '''Return the number of datasets followed by a user.'''
return meta.Session.query(UserFollowingDataset).filter(
- UserFollowingDataset.object_id == object_id).count()
+ UserFollowingDataset.follower_id == follower_id).count()
@classmethod
- def follower_list(cls, object_id):
- '''Return a list of all of the followers of a dataset.'''
+ def followee_list(cls, follower_id):
+ '''Return a list of datasets followed by a user.'''
return meta.Session.query(UserFollowingDataset).filter(
- UserFollowingDataset.object_id == object_id).all()
+ UserFollowingDataset.follower_id == follower_id).all()
+
@classmethod
- def is_following(cls, follower_id, object_id):
- '''Return True if follower_id is currently following object_id, False
- otherwise.
+ def follower_count(cls, dataset_id):
+ '''Return the number of followers of a dataset.'''
+ return meta.Session.query(UserFollowingDataset).filter(
+ UserFollowingDataset.object_id == dataset_id).count()
+
+ @classmethod
+ def follower_list(cls, dataset_id):
+ '''Return a list of followers of a dataset.'''
+ return meta.Session.query(UserFollowingDataset).filter(
+ UserFollowingDataset.object_id == dataset_id).all()
- '''
- return UserFollowingDataset.get(follower_id, object_id) is not None
user_following_dataset_table = sqlalchemy.Table('user_following_dataset',
meta.metadata,
diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css
index 3d9758013ce..6df5b558884 100644
--- a/ckan/public/css/style.css
+++ b/ckan/public/css/style.css
@@ -615,9 +615,9 @@ ul.userlist .badge {
margin-top: 5px;
}
-/* ================== */
-/* = User Read page = */
-/* ================== */
+/* ================================= */
+/* = User Read and Dashboard pages = */
+/* ================================= */
body.user.read #sidebar { display: none; }
body.user.read #content {
@@ -625,11 +625,11 @@ body.user.read #content {
width: 950px;
}
-.user.read .page_heading {
+.user.read .page_heading, .user.dashboard .page_heading {
font-weight: bold;
}
-.user.read .page_heading img.gravatar {
+.user.read .page_heading img.gravatar, .user.dashboard .page_heading img.gravatar {
padding: 2px;
border: solid 1px #ddd;
vertical-align: middle;
@@ -637,7 +637,7 @@ body.user.read #content {
margin-top: -3px;
}
-.user.read .page_heading .fullname {
+.user.read .page_heading .fullname, .user.dashboard .page_heading .fullname {
font-weight: normal;
color: #999;
}
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 2eecaa34bd8..131d7b87d8a 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -491,7 +491,7 @@ CKAN.View.ResourceEditor = Backbone.View.extend({
CKAN.View.Resource = Backbone.View.extend({
initialize: function() {
this.el = $(this.el);
- _.bindAll(this,'updateName','updateIcon','name','askToDelete','openMyPanel','setErrors','setupDynamicExtras','addDynamicExtra', 'onDatastoreEnabledChange');
+ _.bindAll(this,'updateName','updateIcon','name','askToDelete','openMyPanel','setErrors','setupDynamicExtras','addDynamicExtra' );
this.render();
},
render: function() {
@@ -526,12 +526,8 @@ CKAN.View.Resource = Backbone.View.extend({
// Hook to open panel link
this.li.find('.resource-open-my-panel').click(this.openMyPanel);
this.table.find('.js-resource-edit-delete').click(this.askToDelete);
- this.table.find('.js-datastore-enabled-checkbox').change(this.onDatastoreEnabledChange);
// Hook to markdown editor
CKAN.Utils.setupMarkdownEditor(this.table.find('.markdown-editor'));
- if (resource_object.resource.webstore_url) {
- this.table.find('.js-datastore-enabled-checkbox').prop('checked', true);
- }
// Set initial state
this.updateName();
@@ -729,12 +725,6 @@ CKAN.View.Resource = Backbone.View.extend({
removeFromDom: function() {
this.li.remove();
this.table.remove();
- },
- onDatastoreEnabledChange: function(e) {
- var isChecked = this.table.find('.js-datastore-enabled-checkbox').prop('checked');
- var webstore_url = isChecked ? 'enabled' : null;
- this.model.set({webstore_url: webstore_url});
- this.table.find('.js-datastore-enabled-text').val(webstore_url);
}
});
@@ -867,7 +857,6 @@ CKAN.View.ResourceAddUpload = Backbone.View.extend({
, hash: data._checksum
, cache_url: data._location
, cache_url_updated: lastmod
- , webstore_url: data._location
}
, {
error: function(model, error) {
@@ -934,7 +923,6 @@ CKAN.View.ResourceAddUrl = Backbone.View.extend({
size: data.size,
mimetype: data.mimetype,
last_modified: data.last_modified,
- webstore_url: 'enabled',
url_error: (data.url_errors || [""])[0]
});
self.collection.add(newResource);
@@ -944,9 +932,6 @@ CKAN.View.ResourceAddUrl = Backbone.View.extend({
}
else {
newResource.set({url: urlVal, resource_type: this.options.mode});
- if (newResource.get('resource_type')=='file') {
- newResource.set({webstore_url: 'enabled'});
- }
this.collection.add(newResource);
this.resetForm();
}
@@ -1034,7 +1019,7 @@ CKAN.Utils = function($, my) {
input_box.attr('name', new_name);
input_box.attr('id', new_name);
-
+
var $new = $('
');
$new.append($('').attr('name', old_name).val(ui.item.value));
$new.append(' ');
@@ -1443,7 +1428,7 @@ CKAN.Utils = function($, my) {
return;
}
var data = JSON.stringify({
- id: object_id,
+ id: object_id
});
var nextState = 'unfollow';
var nextString = CKAN.Strings.unfollow;
@@ -1457,7 +1442,7 @@ CKAN.Utils = function($, my) {
return;
}
var data = JSON.stringify({
- id: object_id,
+ id: object_id
});
var nextState = 'follow';
var nextString = CKAN.Strings.follow;
@@ -1476,10 +1461,10 @@ CKAN.Utils = function($, my) {
success: function(data) {
button.setAttribute('data-state', nextState);
button.innerHTML = nextString;
- },
+ }
});
};
-
+
// This only needs to happen on dataset pages, but it doesn't seem to do
// any harm to call it anyway.
$('#user_follow_button').on('click', followButtonClicked);
@@ -1585,6 +1570,14 @@ CKAN.DataPreview = function ($, my) {
my.loadPreviewDialog = function(resourceData) {
my.$dialog.html('
Loading ...
');
+ function showError(msg){
+ msg = msg || CKAN.Strings.errorLoadingPreview;
+ return $('#ckanext-datapreview')
+ .append('')
+ .addClass('alert alert-error fade in')
+ .html(msg);
+ }
+
function initializeDataExplorer(dataset) {
var views = [
{
@@ -1618,6 +1611,7 @@ CKAN.DataPreview = function ($, my) {
}
});
+
// -----------------------------
// Setup the Embed modal dialog.
// -----------------------------
@@ -1674,7 +1668,7 @@ CKAN.DataPreview = function ($, my) {
}
// 4 situations
- // a) have a webstore_url
+ // a) webstore_url is active (something was posted to the datastore)
// b) csv or xls (but not webstore)
// c) can be treated as plain text
// d) none of the above but worth iframing (assumption is
@@ -1697,14 +1691,38 @@ CKAN.DataPreview = function ($, my) {
if (resourceData.webstore_url) {
resourceData.elasticsearch_url = '/api/data/' + resourceData.id;
var dataset = new recline.Model.Dataset(resourceData, 'elasticsearch');
- initializeDataExplorer(dataset);
+ var errorMsg = CKAN.Strings.errorLoadingPreview + ': ' + CKAN.Strings.errorDataStore;
+ dataset.fetch()
+ .done(function(dataset){
+ initializeDataExplorer(dataset);
+ })
+ .fail(function(error){
+ if (error.message) errorMsg += ' (' + error.message + ')';
+ showError(errorMsg);
+ });
+
}
else if (resourceData.formatNormalized in {'csv': '', 'xls': ''}) {
// set format as this is used by Recline in setting format for DataProxy
resourceData.format = resourceData.formatNormalized;
var dataset = new recline.Model.Dataset(resourceData, 'dataproxy');
- initializeDataExplorer(dataset);
- $('.recline-query-editor .text-query').hide();
+ var errorMsg = CKAN.Strings.errorLoadingPreview + ': ' +CKAN.Strings.errorDataProxy;
+ dataset.fetch()
+ .done(function(dataset){
+
+ dataset.bind('query:fail', function(error) {
+ $('#ckanext-datapreview .data-view-container').hide();
+ $('#ckanext-datapreview .header').hide();
+ $('.preview-header .btn').hide();
+ });
+
+ initializeDataExplorer(dataset);
+ $('.recline-query-editor .text-query').hide();
+ })
+ .fail(function(error){
+ if (error.message) errorMsg += ' (' + error.message + ')';
+ showError(errorMsg);
+ });
}
else if (resourceData.formatNormalized in {
'rdf+xml': '',
diff --git a/ckan/public/scripts/templates.js b/ckan/public/scripts/templates.js
index 6ecad116a6b..596c94f45fc 100644
--- a/ckan/public/scripts/templates.js
+++ b/ckan/public/scripts/templates.js
@@ -27,7 +27,6 @@ CKAN.Templates.resourceEntry = ' \
';
var youCanUseMarkdownString = CKAN.Strings.youCanUseMarkdown.replace('%a', '').replace('%b', '');
-var shouldADataStoreBeEnabledString = CKAN.Strings.shouldADataStoreBeEnabled.replace('%a', '').replace('%b', '');
var datesAreInISOString = CKAN.Strings.datesAreInISO.replace('%a', '').replace('%b', '').replace('%c', '').replace('%d', '');
// TODO it would be nice to unify this with the markdown editor specified in helpers.py
@@ -93,16 +92,6 @@ CKAN.Templates.resourceDetails = ' \
{{/if}} \
\
\
-
\
- \
-
\
- \
-
\
-
\
\
\
\
diff --git a/ckan/public/scripts/vendor/flot/0.7/excanvas.js b/ckan/public/scripts/vendor/flot/0.7/excanvas.js
new file mode 100644
index 00000000000..c40d6f7014d
--- /dev/null
+++ b/ckan/public/scripts/vendor/flot/0.7/excanvas.js
@@ -0,0 +1,1427 @@
+// Copyright 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Known Issues:
+//
+// * Patterns only support repeat.
+// * Radial gradient are not implemented. The VML version of these look very
+// different from the canvas one.
+// * Clipping paths are not implemented.
+// * Coordsize. The width and height attribute have higher priority than the
+// width and height style values which isn't correct.
+// * Painting mode isn't implemented.
+// * Canvas width/height should is using content-box by default. IE in
+// Quirks mode will draw the canvas using border-box. Either change your
+// doctype to HTML5
+// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
+// or use Box Sizing Behavior from WebFX
+// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
+// * Non uniform scaling does not correctly scale strokes.
+// * Filling very large shapes (above 5000 points) is buggy.
+// * Optimize. There is always room for speed improvements.
+
+// Only add this code if we do not already have a canvas implementation
+if (!document.createElement('canvas').getContext) {
+
+(function() {
+
+ // alias some functions to make (compiled) code shorter
+ var m = Math;
+ var mr = m.round;
+ var ms = m.sin;
+ var mc = m.cos;
+ var abs = m.abs;
+ var sqrt = m.sqrt;
+
+ // this is used for sub pixel precision
+ var Z = 10;
+ var Z2 = Z / 2;
+
+ /**
+ * This funtion is assigned to the