diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 21a87c73948..4954bfb0852 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,6 +1,26 @@
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,#2305] 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. Also 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. There are also API calls for the follow features, see the
+ Action API reference documentation.
+* [#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/routing.py b/ckan/config/routing.py
index 3890032817f..b529d90eed7 100644
--- a/ckan/config/routing.py
+++ b/ckan/config/routing.py
@@ -252,6 +252,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/package.py b/ckan/controllers/package.py
index ef086fc5ae0..ccab6931135 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -594,8 +594,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
@@ -627,7 +626,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 f6f425deec5..e90b4be4938 100644
--- a/ckan/controllers/related.py
+++ b/ckan/controllers/related.py
@@ -34,5 +34,5 @@ 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")
diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py
index 2814cea77c4..30f901d796e 100644
--- a/ckan/controllers/user.py
+++ b/ckan/controllers/user.py
@@ -117,8 +117,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)
@@ -445,3 +445,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 4e29c44da4e..114efb0a78e 100644
--- a/ckan/lib/helpers.py
+++ b/ckan/lib/helpers.py
@@ -506,7 +506,7 @@ def format_icon(_format):
def linked_gravatar(email_hash, size=100, default=None):
return literal(
- '' % _('Update your avatar at gravatar.com') +
'%s' % gravatar(email_hash,size,default)
)
@@ -871,6 +871,20 @@ def follow_count(obj_type, obj_id):
context = {'model' : model, 'session':model.Session, 'user':c.user}
return logic.get_action(action)(context, {'id': obj_id})
+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
@@ -927,6 +941,7 @@ def follow_count(obj_type, obj_id):
'unselected_facet_items',
'follow_button',
'follow_count',
+ 'dashboard_activity_stream',
# imported into ckan.lib.helpers
'literal',
'link_to',
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/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..f15525bd768 100644
--- a/ckan/logic/action/get.py
+++ b/ckan/logic/action/get.py
@@ -1746,6 +1746,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 +1765,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 +1844,7 @@ def user_follower_count(context, data_dict):
:param id: the id or name of the user
:type id: string
+
:rtype: int
'''
@@ -1849,6 +1860,7 @@ def dataset_follower_count(context, data_dict):
:param id: the id or name of the dataset
:type id: string
+
:rtype: int
'''
@@ -1869,7 +1881,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 +1889,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 +1906,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 +1937,7 @@ def am_following_user(context, data_dict):
:param id: the id or name of the user
:type id: string
+
:rtype: boolean
'''
@@ -1940,6 +1955,7 @@ def am_following_dataset(context, data_dict):
:param id: the id or name of the dataset
:type id: string
+
:rtype: boolean
'''
@@ -1951,3 +1967,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 ea371571b0a..f36960b4db0 100644
--- a/ckan/logic/auth/publisher/create.py
+++ b/ckan/logic/auth/publisher/create.py
@@ -79,7 +79,7 @@ def group_create(context, data_dict=None):
model = context['model']
user = context['user']
- if not user:
+ if not model.User.get(user):
return {'success': False, 'msg': _('User is not authorized to create groups') }
if Authorizer.is_sysadmin(user):
diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
index 61a4b4abe2c..8257408bb68 100644
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -49,7 +49,7 @@ def default_resource_schema():
'revision_id': [ignore_missing, unicode],
'resource_group_id': [ignore],
'package_id': [ignore],
- 'url': [ignore_empty, unicode],#, URL(add_http=False)],
+ 'url': [not_empty, unicode],#, URL(add_http=False)],
'description': [ignore_missing, unicode],
'format': [ignore_missing, unicode],
'hash': [ignore_missing, unicode],
diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py
index 30f4fea47f3..d7bc95c15d6 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..e2a36c76961 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -1443,7 +1443,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 +1457,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,7 +1476,7 @@ CKAN.Utils = function($, my) {
success: function(data) {
button.setAttribute('data-state', nextState);
button.innerHTML = nextString;
- },
+ }
});
};
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