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 elements as element.getContext(). + * @this {HTMLElement} + * @return {CanvasRenderingContext2D_} + */ + function getContext() { + return this.context_ || + (this.context_ = new CanvasRenderingContext2D_(this)); + } + + var slice = Array.prototype.slice; + + /** + * Binds a function to an object. The returned function will always use the + * passed in {@code obj} as {@code this}. + * + * Example: + * + * g = bind(f, obj, a, b) + * g(c, d) // will do f.call(obj, a, b, c, d) + * + * @param {Function} f The function to bind the object to + * @param {Object} obj The object that should act as this when the function + * is called + * @param {*} var_args Rest arguments that will be used as the initial + * arguments when the function is called + * @return {Function} A new function that has bound this + */ + function bind(f, obj, var_args) { + var a = slice.call(arguments, 2); + return function() { + return f.apply(obj, a.concat(slice.call(arguments))); + }; + } + + function encodeHtmlAttribute(s) { + return String(s).replace(/&/g, '&').replace(/"/g, '"'); + } + + function addNamespacesAndStylesheet(doc) { + // create xmlns + if (!doc.namespaces['g_vml_']) { + doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml', + '#default#VML'); + + } + if (!doc.namespaces['g_o_']) { + doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office', + '#default#VML'); + } + + // Setup default CSS. Only add one style sheet per document + if (!doc.styleSheets['ex_canvas_']) { + var ss = doc.createStyleSheet(); + ss.owningElement.id = 'ex_canvas_'; + ss.cssText = 'canvas{display:inline-block;overflow:hidden;' + + // default size is 300x150 in Gecko and Opera + 'text-align:left;width:300px;height:150px}'; + } + } + + // Add namespaces and stylesheet at startup. + addNamespacesAndStylesheet(document); + + var G_vmlCanvasManager_ = { + init: function(opt_doc) { + if (/MSIE/.test(navigator.userAgent) && !window.opera) { + var doc = opt_doc || document; + // Create a dummy element so that IE will allow canvas elements to be + // recognized. + doc.createElement('canvas'); + doc.attachEvent('onreadystatechange', bind(this.init_, this, doc)); + } + }, + + init_: function(doc) { + // find all canvas elements + var els = doc.getElementsByTagName('canvas'); + for (var i = 0; i < els.length; i++) { + this.initElement(els[i]); + } + }, + + /** + * Public initializes a canvas element so that it can be used as canvas + * element from now on. This is called automatically before the page is + * loaded but if you are creating elements using createElement you need to + * make sure this is called on the element. + * @param {HTMLElement} el The canvas element to initialize. + * @return {HTMLElement} the element that was created. + */ + initElement: function(el) { + if (!el.getContext) { + el.getContext = getContext; + + // Add namespaces and stylesheet to document of the element. + addNamespacesAndStylesheet(el.ownerDocument); + + // Remove fallback content. There is no way to hide text nodes so we + // just remove all childNodes. We could hide all elements and remove + // text nodes but who really cares about the fallback content. + el.innerHTML = ''; + + // do not use inline function because that will leak memory + el.attachEvent('onpropertychange', onPropertyChange); + el.attachEvent('onresize', onResize); + + var attrs = el.attributes; + if (attrs.width && attrs.width.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setWidth_(attrs.width.nodeValue); + el.style.width = attrs.width.nodeValue + 'px'; + } else { + el.width = el.clientWidth; + } + if (attrs.height && attrs.height.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setHeight_(attrs.height.nodeValue); + el.style.height = attrs.height.nodeValue + 'px'; + } else { + el.height = el.clientHeight; + } + //el.getContext().setCoordsize_() + } + return el; + } + }; + + function onPropertyChange(e) { + var el = e.srcElement; + + switch (e.propertyName) { + case 'width': + el.getContext().clearRect(); + el.style.width = el.attributes.width.nodeValue + 'px'; + // In IE8 this does not trigger onresize. + el.firstChild.style.width = el.clientWidth + 'px'; + break; + case 'height': + el.getContext().clearRect(); + el.style.height = el.attributes.height.nodeValue + 'px'; + el.firstChild.style.height = el.clientHeight + 'px'; + break; + } + } + + function onResize(e) { + var el = e.srcElement; + if (el.firstChild) { + el.firstChild.style.width = el.clientWidth + 'px'; + el.firstChild.style.height = el.clientHeight + 'px'; + } + } + + G_vmlCanvasManager_.init(); + + // precompute "00" to "FF" + var decToHex = []; + for (var i = 0; i < 16; i++) { + for (var j = 0; j < 16; j++) { + decToHex[i * 16 + j] = i.toString(16) + j.toString(16); + } + } + + function createMatrixIdentity() { + return [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ]; + } + + function matrixMultiply(m1, m2) { + var result = createMatrixIdentity(); + + for (var x = 0; x < 3; x++) { + for (var y = 0; y < 3; y++) { + var sum = 0; + + for (var z = 0; z < 3; z++) { + sum += m1[x][z] * m2[z][y]; + } + + result[x][y] = sum; + } + } + return result; + } + + function copyState(o1, o2) { + o2.fillStyle = o1.fillStyle; + o2.lineCap = o1.lineCap; + o2.lineJoin = o1.lineJoin; + o2.lineWidth = o1.lineWidth; + o2.miterLimit = o1.miterLimit; + o2.shadowBlur = o1.shadowBlur; + o2.shadowColor = o1.shadowColor; + o2.shadowOffsetX = o1.shadowOffsetX; + o2.shadowOffsetY = o1.shadowOffsetY; + o2.strokeStyle = o1.strokeStyle; + o2.globalAlpha = o1.globalAlpha; + o2.font = o1.font; + o2.textAlign = o1.textAlign; + o2.textBaseline = o1.textBaseline; + o2.arcScaleX_ = o1.arcScaleX_; + o2.arcScaleY_ = o1.arcScaleY_; + o2.lineScale_ = o1.lineScale_; + } + + var colorData = { + aliceblue: '#F0F8FF', + antiquewhite: '#FAEBD7', + aquamarine: '#7FFFD4', + azure: '#F0FFFF', + beige: '#F5F5DC', + bisque: '#FFE4C4', + black: '#000000', + blanchedalmond: '#FFEBCD', + blueviolet: '#8A2BE2', + brown: '#A52A2A', + burlywood: '#DEB887', + cadetblue: '#5F9EA0', + chartreuse: '#7FFF00', + chocolate: '#D2691E', + coral: '#FF7F50', + cornflowerblue: '#6495ED', + cornsilk: '#FFF8DC', + crimson: '#DC143C', + cyan: '#00FFFF', + darkblue: '#00008B', + darkcyan: '#008B8B', + darkgoldenrod: '#B8860B', + darkgray: '#A9A9A9', + darkgreen: '#006400', + darkgrey: '#A9A9A9', + darkkhaki: '#BDB76B', + darkmagenta: '#8B008B', + darkolivegreen: '#556B2F', + darkorange: '#FF8C00', + darkorchid: '#9932CC', + darkred: '#8B0000', + darksalmon: '#E9967A', + darkseagreen: '#8FBC8F', + darkslateblue: '#483D8B', + darkslategray: '#2F4F4F', + darkslategrey: '#2F4F4F', + darkturquoise: '#00CED1', + darkviolet: '#9400D3', + deeppink: '#FF1493', + deepskyblue: '#00BFFF', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1E90FF', + firebrick: '#B22222', + floralwhite: '#FFFAF0', + forestgreen: '#228B22', + gainsboro: '#DCDCDC', + ghostwhite: '#F8F8FF', + gold: '#FFD700', + goldenrod: '#DAA520', + grey: '#808080', + greenyellow: '#ADFF2F', + honeydew: '#F0FFF0', + hotpink: '#FF69B4', + indianred: '#CD5C5C', + indigo: '#4B0082', + ivory: '#FFFFF0', + khaki: '#F0E68C', + lavender: '#E6E6FA', + lavenderblush: '#FFF0F5', + lawngreen: '#7CFC00', + lemonchiffon: '#FFFACD', + lightblue: '#ADD8E6', + lightcoral: '#F08080', + lightcyan: '#E0FFFF', + lightgoldenrodyellow: '#FAFAD2', + lightgreen: '#90EE90', + lightgrey: '#D3D3D3', + lightpink: '#FFB6C1', + lightsalmon: '#FFA07A', + lightseagreen: '#20B2AA', + lightskyblue: '#87CEFA', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#B0C4DE', + lightyellow: '#FFFFE0', + limegreen: '#32CD32', + linen: '#FAF0E6', + magenta: '#FF00FF', + mediumaquamarine: '#66CDAA', + mediumblue: '#0000CD', + mediumorchid: '#BA55D3', + mediumpurple: '#9370DB', + mediumseagreen: '#3CB371', + mediumslateblue: '#7B68EE', + mediumspringgreen: '#00FA9A', + mediumturquoise: '#48D1CC', + mediumvioletred: '#C71585', + midnightblue: '#191970', + mintcream: '#F5FFFA', + mistyrose: '#FFE4E1', + moccasin: '#FFE4B5', + navajowhite: '#FFDEAD', + oldlace: '#FDF5E6', + olivedrab: '#6B8E23', + orange: '#FFA500', + orangered: '#FF4500', + orchid: '#DA70D6', + palegoldenrod: '#EEE8AA', + palegreen: '#98FB98', + paleturquoise: '#AFEEEE', + palevioletred: '#DB7093', + papayawhip: '#FFEFD5', + peachpuff: '#FFDAB9', + peru: '#CD853F', + pink: '#FFC0CB', + plum: '#DDA0DD', + powderblue: '#B0E0E6', + rosybrown: '#BC8F8F', + royalblue: '#4169E1', + saddlebrown: '#8B4513', + salmon: '#FA8072', + sandybrown: '#F4A460', + seagreen: '#2E8B57', + seashell: '#FFF5EE', + sienna: '#A0522D', + skyblue: '#87CEEB', + slateblue: '#6A5ACD', + slategray: '#708090', + slategrey: '#708090', + snow: '#FFFAFA', + springgreen: '#00FF7F', + steelblue: '#4682B4', + tan: '#D2B48C', + thistle: '#D8BFD8', + tomato: '#FF6347', + turquoise: '#40E0D0', + violet: '#EE82EE', + wheat: '#F5DEB3', + whitesmoke: '#F5F5F5', + yellowgreen: '#9ACD32' + }; + + + function getRgbHslContent(styleString) { + var start = styleString.indexOf('(', 3); + var end = styleString.indexOf(')', start + 1); + var parts = styleString.substring(start + 1, end).split(','); + // add alpha if needed + if (parts.length == 4 && styleString.substr(3, 1) == 'a') { + alpha = Number(parts[3]); + } else { + parts[3] = 1; + } + return parts; + } + + function percent(s) { + return parseFloat(s) / 100; + } + + function clamp(v, min, max) { + return Math.min(max, Math.max(min, v)); + } + + function hslToRgb(parts){ + var r, g, b; + h = parseFloat(parts[0]) / 360 % 360; + if (h < 0) + h++; + s = clamp(percent(parts[1]), 0, 1); + l = clamp(percent(parts[2]), 0, 1); + if (s == 0) { + r = g = b = l; // achromatic + } else { + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hueToRgb(p, q, h + 1 / 3); + g = hueToRgb(p, q, h); + b = hueToRgb(p, q, h - 1 / 3); + } + + return '#' + decToHex[Math.floor(r * 255)] + + decToHex[Math.floor(g * 255)] + + decToHex[Math.floor(b * 255)]; + } + + function hueToRgb(m1, m2, h) { + if (h < 0) + h++; + if (h > 1) + h--; + + if (6 * h < 1) + return m1 + (m2 - m1) * 6 * h; + else if (2 * h < 1) + return m2; + else if (3 * h < 2) + return m1 + (m2 - m1) * (2 / 3 - h) * 6; + else + return m1; + } + + function processStyle(styleString) { + var str, alpha = 1; + + styleString = String(styleString); + if (styleString.charAt(0) == '#') { + str = styleString; + } else if (/^rgb/.test(styleString)) { + var parts = getRgbHslContent(styleString); + var str = '#', n; + for (var i = 0; i < 3; i++) { + if (parts[i].indexOf('%') != -1) { + n = Math.floor(percent(parts[i]) * 255); + } else { + n = Number(parts[i]); + } + str += decToHex[clamp(n, 0, 255)]; + } + alpha = parts[3]; + } else if (/^hsl/.test(styleString)) { + var parts = getRgbHslContent(styleString); + str = hslToRgb(parts); + alpha = parts[3]; + } else { + str = colorData[styleString] || styleString; + } + return {color: str, alpha: alpha}; + } + + var DEFAULT_STYLE = { + style: 'normal', + variant: 'normal', + weight: 'normal', + size: 10, + family: 'sans-serif' + }; + + // Internal text style cache + var fontStyleCache = {}; + + function processFontStyle(styleString) { + if (fontStyleCache[styleString]) { + return fontStyleCache[styleString]; + } + + var el = document.createElement('div'); + var style = el.style; + try { + style.font = styleString; + } catch (ex) { + // Ignore failures to set to invalid font. + } + + return fontStyleCache[styleString] = { + style: style.fontStyle || DEFAULT_STYLE.style, + variant: style.fontVariant || DEFAULT_STYLE.variant, + weight: style.fontWeight || DEFAULT_STYLE.weight, + size: style.fontSize || DEFAULT_STYLE.size, + family: style.fontFamily || DEFAULT_STYLE.family + }; + } + + function getComputedStyle(style, element) { + var computedStyle = {}; + + for (var p in style) { + computedStyle[p] = style[p]; + } + + // Compute the size + var canvasFontSize = parseFloat(element.currentStyle.fontSize), + fontSize = parseFloat(style.size); + + if (typeof style.size == 'number') { + computedStyle.size = style.size; + } else if (style.size.indexOf('px') != -1) { + computedStyle.size = fontSize; + } else if (style.size.indexOf('em') != -1) { + computedStyle.size = canvasFontSize * fontSize; + } else if(style.size.indexOf('%') != -1) { + computedStyle.size = (canvasFontSize / 100) * fontSize; + } else if (style.size.indexOf('pt') != -1) { + computedStyle.size = fontSize / .75; + } else { + computedStyle.size = canvasFontSize; + } + + // Different scaling between normal text and VML text. This was found using + // trial and error to get the same size as non VML text. + computedStyle.size *= 0.981; + + return computedStyle; + } + + function buildStyle(style) { + return style.style + ' ' + style.variant + ' ' + style.weight + ' ' + + style.size + 'px ' + style.family; + } + + function processLineCap(lineCap) { + switch (lineCap) { + case 'butt': + return 'flat'; + case 'round': + return 'round'; + case 'square': + default: + return 'square'; + } + } + + /** + * This class implements CanvasRenderingContext2D interface as described by + * the WHATWG. + * @param {HTMLElement} surfaceElement The element that the 2D context should + * be associated with + */ + function CanvasRenderingContext2D_(surfaceElement) { + this.m_ = createMatrixIdentity(); + + this.mStack_ = []; + this.aStack_ = []; + this.currentPath_ = []; + + // Canvas context properties + this.strokeStyle = '#000'; + this.fillStyle = '#000'; + + this.lineWidth = 1; + this.lineJoin = 'miter'; + this.lineCap = 'butt'; + this.miterLimit = Z * 1; + this.globalAlpha = 1; + this.font = '10px sans-serif'; + this.textAlign = 'left'; + this.textBaseline = 'alphabetic'; + this.canvas = surfaceElement; + + var el = surfaceElement.ownerDocument.createElement('div'); + el.style.width = surfaceElement.clientWidth + 'px'; + el.style.height = surfaceElement.clientHeight + 'px'; + el.style.overflow = 'hidden'; + el.style.position = 'absolute'; + surfaceElement.appendChild(el); + + this.element_ = el; + this.arcScaleX_ = 1; + this.arcScaleY_ = 1; + this.lineScale_ = 1; + } + + var contextPrototype = CanvasRenderingContext2D_.prototype; + contextPrototype.clearRect = function() { + if (this.textMeasureEl_) { + this.textMeasureEl_.removeNode(true); + this.textMeasureEl_ = null; + } + this.element_.innerHTML = ''; + }; + + contextPrototype.beginPath = function() { + // TODO: Branch current matrix so that save/restore has no effect + // as per safari docs. + this.currentPath_ = []; + }; + + contextPrototype.moveTo = function(aX, aY) { + var p = this.getCoords_(aX, aY); + this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y}); + this.currentX_ = p.x; + this.currentY_ = p.y; + }; + + contextPrototype.lineTo = function(aX, aY) { + var p = this.getCoords_(aX, aY); + this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y}); + + this.currentX_ = p.x; + this.currentY_ = p.y; + }; + + contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, + aCP2x, aCP2y, + aX, aY) { + var p = this.getCoords_(aX, aY); + var cp1 = this.getCoords_(aCP1x, aCP1y); + var cp2 = this.getCoords_(aCP2x, aCP2y); + bezierCurveTo(this, cp1, cp2, p); + }; + + // Helper function that takes the already fixed cordinates. + function bezierCurveTo(self, cp1, cp2, p) { + self.currentPath_.push({ + type: 'bezierCurveTo', + cp1x: cp1.x, + cp1y: cp1.y, + cp2x: cp2.x, + cp2y: cp2.y, + x: p.x, + y: p.y + }); + self.currentX_ = p.x; + self.currentY_ = p.y; + } + + contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { + // the following is lifted almost directly from + // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes + + var cp = this.getCoords_(aCPx, aCPy); + var p = this.getCoords_(aX, aY); + + var cp1 = { + x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_), + y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_) + }; + var cp2 = { + x: cp1.x + (p.x - this.currentX_) / 3.0, + y: cp1.y + (p.y - this.currentY_) / 3.0 + }; + + bezierCurveTo(this, cp1, cp2, p); + }; + + contextPrototype.arc = function(aX, aY, aRadius, + aStartAngle, aEndAngle, aClockwise) { + aRadius *= Z; + var arcType = aClockwise ? 'at' : 'wa'; + + var xStart = aX + mc(aStartAngle) * aRadius - Z2; + var yStart = aY + ms(aStartAngle) * aRadius - Z2; + + var xEnd = aX + mc(aEndAngle) * aRadius - Z2; + var yEnd = aY + ms(aEndAngle) * aRadius - Z2; + + // IE won't render arches drawn counter clockwise if xStart == xEnd. + if (xStart == xEnd && !aClockwise) { + xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something + // that can be represented in binary + } + + var p = this.getCoords_(aX, aY); + var pStart = this.getCoords_(xStart, yStart); + var pEnd = this.getCoords_(xEnd, yEnd); + + this.currentPath_.push({type: arcType, + x: p.x, + y: p.y, + radius: aRadius, + xStart: pStart.x, + yStart: pStart.y, + xEnd: pEnd.x, + yEnd: pEnd.y}); + + }; + + contextPrototype.rect = function(aX, aY, aWidth, aHeight) { + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + }; + + contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { + var oldPath = this.currentPath_; + this.beginPath(); + + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.stroke(); + + this.currentPath_ = oldPath; + }; + + contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { + var oldPath = this.currentPath_; + this.beginPath(); + + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.fill(); + + this.currentPath_ = oldPath; + }; + + contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { + var gradient = new CanvasGradient_('gradient'); + gradient.x0_ = aX0; + gradient.y0_ = aY0; + gradient.x1_ = aX1; + gradient.y1_ = aY1; + return gradient; + }; + + contextPrototype.createRadialGradient = function(aX0, aY0, aR0, + aX1, aY1, aR1) { + var gradient = new CanvasGradient_('gradientradial'); + gradient.x0_ = aX0; + gradient.y0_ = aY0; + gradient.r0_ = aR0; + gradient.x1_ = aX1; + gradient.y1_ = aY1; + gradient.r1_ = aR1; + return gradient; + }; + + contextPrototype.drawImage = function(image, var_args) { + var dx, dy, dw, dh, sx, sy, sw, sh; + + // to find the original width we overide the width and height + var oldRuntimeWidth = image.runtimeStyle.width; + var oldRuntimeHeight = image.runtimeStyle.height; + image.runtimeStyle.width = 'auto'; + image.runtimeStyle.height = 'auto'; + + // get the original size + var w = image.width; + var h = image.height; + + // and remove overides + image.runtimeStyle.width = oldRuntimeWidth; + image.runtimeStyle.height = oldRuntimeHeight; + + if (arguments.length == 3) { + dx = arguments[1]; + dy = arguments[2]; + sx = sy = 0; + sw = dw = w; + sh = dh = h; + } else if (arguments.length == 5) { + dx = arguments[1]; + dy = arguments[2]; + dw = arguments[3]; + dh = arguments[4]; + sx = sy = 0; + sw = w; + sh = h; + } else if (arguments.length == 9) { + sx = arguments[1]; + sy = arguments[2]; + sw = arguments[3]; + sh = arguments[4]; + dx = arguments[5]; + dy = arguments[6]; + dw = arguments[7]; + dh = arguments[8]; + } else { + throw Error('Invalid number of arguments'); + } + + var d = this.getCoords_(dx, dy); + + var w2 = sw / 2; + var h2 = sh / 2; + + var vmlStr = []; + + var W = 10; + var H = 10; + + // For some reason that I've now forgotten, using divs didn't work + vmlStr.push(' ' , + '', + ''); + + this.element_.insertAdjacentHTML('BeforeEnd', vmlStr.join('')); + }; + + contextPrototype.stroke = function(aFill) { + var W = 10; + var H = 10; + // Divide the shape into chunks if it's too long because IE has a limit + // somewhere for how long a VML shape can be. This simple division does + // not work with fills, only strokes, unfortunately. + var chunkSize = 5000; + + var min = {x: null, y: null}; + var max = {x: null, y: null}; + + for (var j = 0; j < this.currentPath_.length; j += chunkSize) { + var lineStr = []; + var lineOpen = false; + + lineStr.push(''); + + if (!aFill) { + appendStroke(this, lineStr); + } else { + appendFill(this, lineStr, min, max); + } + + lineStr.push(''); + + this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); + } + }; + + function appendStroke(ctx, lineStr) { + var a = processStyle(ctx.strokeStyle); + var color = a.color; + var opacity = a.alpha * ctx.globalAlpha; + var lineWidth = ctx.lineScale_ * ctx.lineWidth; + + // VML cannot correctly render a line if the width is less than 1px. + // In that case, we dilute the color to make the line look thinner. + if (lineWidth < 1) { + opacity *= lineWidth; + } + + lineStr.push( + '' + ); + } + + function appendFill(ctx, lineStr, min, max) { + var fillStyle = ctx.fillStyle; + var arcScaleX = ctx.arcScaleX_; + var arcScaleY = ctx.arcScaleY_; + var width = max.x - min.x; + var height = max.y - min.y; + if (fillStyle instanceof CanvasGradient_) { + // TODO: Gradients transformed with the transformation matrix. + var angle = 0; + var focus = {x: 0, y: 0}; + + // additional offset + var shift = 0; + // scale factor for offset + var expansion = 1; + + if (fillStyle.type_ == 'gradient') { + var x0 = fillStyle.x0_ / arcScaleX; + var y0 = fillStyle.y0_ / arcScaleY; + var x1 = fillStyle.x1_ / arcScaleX; + var y1 = fillStyle.y1_ / arcScaleY; + var p0 = ctx.getCoords_(x0, y0); + var p1 = ctx.getCoords_(x1, y1); + var dx = p1.x - p0.x; + var dy = p1.y - p0.y; + angle = Math.atan2(dx, dy) * 180 / Math.PI; + + // The angle should be a non-negative number. + if (angle < 0) { + angle += 360; + } + + // Very small angles produce an unexpected result because they are + // converted to a scientific notation string. + if (angle < 1e-6) { + angle = 0; + } + } else { + var p0 = ctx.getCoords_(fillStyle.x0_, fillStyle.y0_); + focus = { + x: (p0.x - min.x) / width, + y: (p0.y - min.y) / height + }; + + width /= arcScaleX * Z; + height /= arcScaleY * Z; + var dimension = m.max(width, height); + shift = 2 * fillStyle.r0_ / dimension; + expansion = 2 * fillStyle.r1_ / dimension - shift; + } + + // We need to sort the color stops in ascending order by offset, + // otherwise IE won't interpret it correctly. + var stops = fillStyle.colors_; + stops.sort(function(cs1, cs2) { + return cs1.offset - cs2.offset; + }); + + var length = stops.length; + var color1 = stops[0].color; + var color2 = stops[length - 1].color; + var opacity1 = stops[0].alpha * ctx.globalAlpha; + var opacity2 = stops[length - 1].alpha * ctx.globalAlpha; + + var colors = []; + for (var i = 0; i < length; i++) { + var stop = stops[i]; + colors.push(stop.offset * expansion + shift + ' ' + stop.color); + } + + // When colors attribute is used, the meanings of opacity and o:opacity2 + // are reversed. + lineStr.push(''); + } else if (fillStyle instanceof CanvasPattern_) { + if (width && height) { + var deltaLeft = -min.x; + var deltaTop = -min.y; + lineStr.push(''); + } + } else { + var a = processStyle(ctx.fillStyle); + var color = a.color; + var opacity = a.alpha * ctx.globalAlpha; + lineStr.push(''); + } + } + + contextPrototype.fill = function() { + this.stroke(true); + }; + + contextPrototype.closePath = function() { + this.currentPath_.push({type: 'close'}); + }; + + /** + * @private + */ + contextPrototype.getCoords_ = function(aX, aY) { + var m = this.m_; + return { + x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2, + y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2 + }; + }; + + contextPrototype.save = function() { + var o = {}; + copyState(this, o); + this.aStack_.push(o); + this.mStack_.push(this.m_); + this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); + }; + + contextPrototype.restore = function() { + if (this.aStack_.length) { + copyState(this.aStack_.pop(), this); + this.m_ = this.mStack_.pop(); + } + }; + + function matrixIsFinite(m) { + return isFinite(m[0][0]) && isFinite(m[0][1]) && + isFinite(m[1][0]) && isFinite(m[1][1]) && + isFinite(m[2][0]) && isFinite(m[2][1]); + } + + function setM(ctx, m, updateLineScale) { + if (!matrixIsFinite(m)) { + return; + } + ctx.m_ = m; + + if (updateLineScale) { + // Get the line scale. + // Determinant of this.m_ means how much the area is enlarged by the + // transformation. So its square root can be used as a scale factor + // for width. + var det = m[0][0] * m[1][1] - m[0][1] * m[1][0]; + ctx.lineScale_ = sqrt(abs(det)); + } + } + + contextPrototype.translate = function(aX, aY) { + var m1 = [ + [1, 0, 0], + [0, 1, 0], + [aX, aY, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), false); + }; + + contextPrototype.rotate = function(aRot) { + var c = mc(aRot); + var s = ms(aRot); + + var m1 = [ + [c, s, 0], + [-s, c, 0], + [0, 0, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), false); + }; + + contextPrototype.scale = function(aX, aY) { + this.arcScaleX_ *= aX; + this.arcScaleY_ *= aY; + var m1 = [ + [aX, 0, 0], + [0, aY, 0], + [0, 0, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), true); + }; + + contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) { + var m1 = [ + [m11, m12, 0], + [m21, m22, 0], + [dx, dy, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), true); + }; + + contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) { + var m = [ + [m11, m12, 0], + [m21, m22, 0], + [dx, dy, 1] + ]; + + setM(this, m, true); + }; + + /** + * The text drawing function. + * The maxWidth argument isn't taken in account, since no browser supports + * it yet. + */ + contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) { + var m = this.m_, + delta = 1000, + left = 0, + right = delta, + offset = {x: 0, y: 0}, + lineStr = []; + + var fontStyle = getComputedStyle(processFontStyle(this.font), + this.element_); + + var fontStyleString = buildStyle(fontStyle); + + var elementStyle = this.element_.currentStyle; + var textAlign = this.textAlign.toLowerCase(); + switch (textAlign) { + case 'left': + case 'center': + case 'right': + break; + case 'end': + textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left'; + break; + case 'start': + textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left'; + break; + default: + textAlign = 'left'; + } + + // 1.75 is an arbitrary number, as there is no info about the text baseline + switch (this.textBaseline) { + case 'hanging': + case 'top': + offset.y = fontStyle.size / 1.75; + break; + case 'middle': + break; + default: + case null: + case 'alphabetic': + case 'ideographic': + case 'bottom': + offset.y = -fontStyle.size / 2.25; + break; + } + + switch(textAlign) { + case 'right': + left = delta; + right = 0.05; + break; + case 'center': + left = right = delta / 2; + break; + } + + var d = this.getCoords_(x + offset.x, y + offset.y); + + lineStr.push(''); + + if (stroke) { + appendStroke(this, lineStr); + } else { + // TODO: Fix the min and max params. + appendFill(this, lineStr, {x: -left, y: 0}, + {x: right, y: fontStyle.size}); + } + + var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' + + m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0'; + + var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z); + + lineStr.push('', + '', + ''); + + this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); + }; + + contextPrototype.fillText = function(text, x, y, maxWidth) { + this.drawText_(text, x, y, maxWidth, false); + }; + + contextPrototype.strokeText = function(text, x, y, maxWidth) { + this.drawText_(text, x, y, maxWidth, true); + }; + + contextPrototype.measureText = function(text) { + if (!this.textMeasureEl_) { + var s = ''; + this.element_.insertAdjacentHTML('beforeEnd', s); + this.textMeasureEl_ = this.element_.lastChild; + } + var doc = this.element_.ownerDocument; + this.textMeasureEl_.innerHTML = ''; + this.textMeasureEl_.style.font = this.font; + // Don't use innerHTML or innerText because they allow markup/whitespace. + this.textMeasureEl_.appendChild(doc.createTextNode(text)); + return {width: this.textMeasureEl_.offsetWidth}; + }; + + /******** STUBS ********/ + contextPrototype.clip = function() { + // TODO: Implement + }; + + contextPrototype.arcTo = function() { + // TODO: Implement + }; + + contextPrototype.createPattern = function(image, repetition) { + return new CanvasPattern_(image, repetition); + }; + + // Gradient / Pattern Stubs + function CanvasGradient_(aType) { + this.type_ = aType; + this.x0_ = 0; + this.y0_ = 0; + this.r0_ = 0; + this.x1_ = 0; + this.y1_ = 0; + this.r1_ = 0; + this.colors_ = []; + } + + CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { + aColor = processStyle(aColor); + this.colors_.push({offset: aOffset, + color: aColor.color, + alpha: aColor.alpha}); + }; + + function CanvasPattern_(image, repetition) { + assertImageIsValid(image); + switch (repetition) { + case 'repeat': + case null: + case '': + this.repetition_ = 'repeat'; + break + case 'repeat-x': + case 'repeat-y': + case 'no-repeat': + this.repetition_ = repetition; + break; + default: + throwException('SYNTAX_ERR'); + } + + this.src_ = image.src; + this.width_ = image.width; + this.height_ = image.height; + } + + function throwException(s) { + throw new DOMException_(s); + } + + function assertImageIsValid(img) { + if (!img || img.nodeType != 1 || img.tagName != 'IMG') { + throwException('TYPE_MISMATCH_ERR'); + } + if (img.readyState != 'complete') { + throwException('INVALID_STATE_ERR'); + } + } + + function DOMException_(s) { + this.code = this[s]; + this.message = s +': DOM Exception ' + this.code; + } + var p = DOMException_.prototype = new Error; + p.INDEX_SIZE_ERR = 1; + p.DOMSTRING_SIZE_ERR = 2; + p.HIERARCHY_REQUEST_ERR = 3; + p.WRONG_DOCUMENT_ERR = 4; + p.INVALID_CHARACTER_ERR = 5; + p.NO_DATA_ALLOWED_ERR = 6; + p.NO_MODIFICATION_ALLOWED_ERR = 7; + p.NOT_FOUND_ERR = 8; + p.NOT_SUPPORTED_ERR = 9; + p.INUSE_ATTRIBUTE_ERR = 10; + p.INVALID_STATE_ERR = 11; + p.SYNTAX_ERR = 12; + p.INVALID_MODIFICATION_ERR = 13; + p.NAMESPACE_ERR = 14; + p.INVALID_ACCESS_ERR = 15; + p.VALIDATION_ERR = 16; + p.TYPE_MISMATCH_ERR = 17; + + // set up externs + G_vmlCanvasManager = G_vmlCanvasManager_; + CanvasRenderingContext2D = CanvasRenderingContext2D_; + CanvasGradient = CanvasGradient_; + CanvasPattern = CanvasPattern_; + DOMException = DOMException_; +})(); + +} // if diff --git a/ckan/public/scripts/vendor/html5shiv/html5.js b/ckan/public/scripts/vendor/html5shiv/html5.js index 74c9564f9ac..7656f7a019c 100644 --- a/ckan/public/scripts/vendor/html5shiv/html5.js +++ b/ckan/public/scripts/vendor/html5shiv/html5.js @@ -1,3 +1,7 @@ -/*! HTML5 Shiv pre3.5 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed - Uncompressed source: https://github.com/aFarkas/html5shiv */ -(function(a,b){function h(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function i(){var a=l.elements;return typeof a=="string"?a.split(" "):a}function j(a){var b={},c=a.createElement,f=a.createDocumentFragment,g=f();a.createElement=function(a){l.shivMethods||c(a);var f;return b[a]?f=b[a].cloneNode():e.test(a)?f=(b[a]=c(a)).cloneNode():f=c(a),f.canHaveChildren&&!d.test(a)?g.appendChild(f):f},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+i().join().replace(/\w+/g,function(a){return b[a]=c(a),g.createElement(a),'c("'+a+'")'})+");return n}")(l,g)}function k(a){var b;return a.documentShived?a:(l.shivCSS&&!f&&(b=!!h(a,"article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio{display:none}canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden]{display:none}audio[controls]{display:inline-block;*display:inline;*zoom:1}mark{background:#FF0;color:#000}")),g||(b=!j(a)),b&&(a.documentShived=b),a)}function p(a){var b,c=a.getElementsByTagName("*"),d=c.length,e=RegExp("^(?:"+i().join("|")+")$","i"),f=[];while(d--)b=c[d],e.test(b.nodeName)&&f.push(b.applyElement(q(b)));return f}function q(a){var b,c=a.attributes,d=c.length,e=a.ownerDocument.createElement(n+":"+a.nodeName);while(d--)b=c[d],b.specified&&e.setAttribute(b.nodeName,b.nodeValue);return e.style.cssText=a.style.cssText,e}function r(a){var b,c=a.split("{"),d=c.length,e=RegExp("(^|[\\s,>+~])("+i().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),f="$1"+n+"\\:$2";while(d--)b=c[d]=c[d].split("}"),b[b.length-1]=b[b.length-1].replace(e,f),c[d]=b.join("}");return c.join("{")}function s(a){var b=a.length;while(b--)a[b].removeNode()}function t(a){var b,c,d=a.namespaces,e=a.parentWindow;return!o||a.printShived?a:(typeof d[n]=="undefined"&&d.add(n),e.attachEvent("onbeforeprint",function(){var d,e,f,g=a.styleSheets,i=[],j=g.length,k=Array(j);while(j--)k[j]=g[j];while(f=k.pop())if(!f.disabled&&m.test(f.media)){for(d=f.imports,j=0,e=d.length;j",f="hidden"in c,f&&typeof injectElementWithStyles=="function"&&injectElementWithStyles("#modernizr{}",function(b){b.hidden=!0,f=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle).display=="none"}),g=c.childNodes.length==1||function(){try{b.createElement("a")}catch(a){return!0}var c=b.createDocumentFragment();return typeof c.cloneNode=="undefined"||typeof c.createDocumentFragment=="undefined"||typeof c.createElement=="undefined"}()})();var l={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:k};a.html5=l,k(b);var m=/^$|\b(?:all|print)\b/,n="html5shiv",o=!g&&function(){var c=b.documentElement;return typeof b.namespaces!="undefined"&&typeof b.parentWindow!="undefined"&&typeof c.applyElement!="undefined"&&typeof c.removeNode!="undefined"&&typeof a.attachEvent!="undefined"}();l.type+=" print",l.shivPrint=t,t(b)})(this,document) \ No newline at end of file +/*! HTML5 Shiv v3.6RC1 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed + Uncompressed source: https://github.com/aFarkas/html5shiv */ +(function(l,f){function m(){var a=e.elements;return"string"==typeof a?a.split(" "):a}function i(a){var b=n[a[o]];b||(b={},h++,a[o]=h,n[h]=b);return b}function p(a,b,c){b||(b=f);if(g)return b.createElement(a);c||(c=i(b));b=c.cache[a]?c.cache[a].cloneNode():r.test(a)?(c.cache[a]=c.createElem(a)).cloneNode():c.createElem(a);return b.canHaveChildren&&!s.test(a)?c.frag.appendChild(b):b}function t(a,b){if(!b.cache)b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag(); +a.createElement=function(c){return!e.shivMethods?b.createElem(c):p(c,a,b)};a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+m().join().replace(/\w+/g,function(a){b.createElem(a);b.frag.createElement(a);return'c("'+a+'")'})+");return n}")(e,b.frag)}function q(a){a||(a=f);var b=i(a);if(e.shivCSS&&!j&&!b.hasCSS){var c,d=a;c=d.createElement("p");d=d.getElementsByTagName("head")[0]||d.documentElement;c.innerHTML="x"; +c=d.insertBefore(c.lastChild,d.firstChild);b.hasCSS=!!c}g||t(a,b);return a}var k=l.html5||{},s=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,r=/^<|^(?:a|b|button|code|div|fieldset|form|h1|h2|h3|h4|h5|h6|i|iframe|img|input|label|li|link|ol|option|p|param|q|script|select|span|strong|style|table|tbody|td|textarea|tfoot|th|thead|tr|ul)$/i,j,o="_html5shiv",h=0,n={},g;(function(){try{var a=f.createElement("a");a.innerHTML="";j="hidden"in a;var b;if(!(b=1==a.childNodes.length)){f.createElement("a"); +var c=f.createDocumentFragment();b="undefined"==typeof c.cloneNode||"undefined"==typeof c.createDocumentFragment||"undefined"==typeof c.createElement}g=b}catch(d){g=j=!0}})();var e={elements:k.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:!1!==k.shivCSS,supportsUnknownElements:g,shivMethods:!1!==k.shivMethods,type:"default",shivDocument:q,createElement:p,createDocumentFragment:function(a, +b){a||(a=f);if(g)return a.createDocumentFragment();for(var b=b||i(a),c=b.frag.cloneNode(),d=0,e=m(),h=e.length;d +${h.activity_div( + template=_("{actor} started following {object}"), + activity=activity, + actor=h.linked_user(activity.user_id), + object=h.dataset_link(activity.data.dataset), + )} + diff --git a/ckan/templates/activity_streams/follow_user.html b/ckan/templates/activity_streams/follow_user.html new file mode 100644 index 00000000000..f0e22d1f64b --- /dev/null +++ b/ckan/templates/activity_streams/follow_user.html @@ -0,0 +1,14 @@ + +${h.activity_div( + template=_("{actor} started following {object}"), + activity=activity, + actor=h.linked_user(activity.user_id), + object=h.linked_user(activity.data.user.name), + )} + diff --git a/ckan/templates/user/dashboard.html b/ckan/templates/user/dashboard.html new file mode 100644 index 00000000000..289d6516769 --- /dev/null +++ b/ckan/templates/user/dashboard.html @@ -0,0 +1,36 @@ + + + ${c.user} - Dashboard - User + + ${h.linked_gravatar(c.user_dict['email_hash'],48)} + ${c.user_dict['name']} + + (${c.user_dict['fullname']}) + + + + +
+

What's going on?

+
+ + ${h.dashboard_activity_stream(c.user_dict['id'])} +
+
+ +
+

Nothing new on CKAN?

+

So, why don't you ...

+ +
+ + + diff --git a/ckan/templates/user/layout.html b/ckan/templates/user/layout.html index 9524379ad5e..e733d4df336 100644 --- a/ckan/templates/user/layout.html +++ b/ckan/templates/user/layout.html @@ -8,7 +8,8 @@ diff --git a/ckanext/stats/controller.py b/ckanext/stats/controller.py index 4e86ab1d98d..37114037b0a 100644 --- a/ckanext/stats/controller.py +++ b/ckanext/stats/controller.py @@ -17,6 +17,21 @@ def index(self): c.deleted_packages_by_week = rev_stats.get_by_week('deleted_packages') c.num_packages_by_week = rev_stats.get_num_packages_by_week() c.package_revisions_by_week = rev_stats.get_by_week('package_revisions') + + c.packages_by_week = []; + for week_date, num_packages, cumulative_num_packages in c.num_packages_by_week: + c.packages_by_week.append('[new Date(%s), %s]' % (week_date.replace('-', ','), cumulative_num_packages)); + + + c.all_package_revisions = []; + for week_date, revs, num_revisions, cumulative_num_revisions in c.package_revisions_by_week: + c.all_package_revisions.append('[new Date(%s), %s]' % (week_date.replace('-', ','), num_revisions)); + + c.new_datasets = [] + for week_date, pkgs, num_packages, cumulative_num_packages in c.new_packages_by_week: + c.new_datasets.append('[new Date(%s), %s]' % (week_date.replace('-', ','), num_packages)); + + return p.toolkit.render('ckanext/stats/index.html') def leaderboard(self, id=None): diff --git a/ckanext/stats/templates/ckanext/stats/index.html b/ckanext/stats/templates/ckanext/stats/index.html index 681527d330d..927ca1add18 100644 --- a/ckanext/stats/templates/ckanext/stats/index.html +++ b/ckanext/stats/templates/ckanext/stats/index.html @@ -9,6 +9,15 @@ Statistics + + + + + @@ -47,8 +61,8 @@

Revisions to Datasets per week

Top Rated Datasets

- - +
DatasetAverage ratingNumber of ratings
+ @@ -56,32 +70,32 @@

Top Rated Datasets

No ratings

Most Edited Datasets

-
DatasetAverage ratingNumber of ratings
${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}${rating}${num_ratings}
- +
DatasetNumber of edits
+
DatasetNumber of edits
${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}${edits}

Largest Groups

- - +
GroupNumber of datasets
+
GroupNumber of datasets
${h.link_to(group.title or group.name, h.url_for(controller='group', action='read', id=group.name))}${num_packages}

Top Tags

- +
- +
${h.link_to(tag.name, h.url_for(controller='tag', action='read', id=tag.name))}${num_packages}${h.link_to(tag.name, h.url_for(controller='tag', action='read', id=tag.name))}${num_packages}

Users owning most datasets

- +
- +
${h.linked_user(user)}${num_packages}${h.linked_user(user)}${num_packages}
@@ -100,8 +114,8 @@

Users owning most datasets

$('body').addClass('no-sidebar'); - - + ${jsConditionalForIe(8, '<script language="javascript" type="text/javascript" src="' + h.url_for_static('/scripts/vendor/flot/0.7/excanvas.js') + '"></script>', 'lte')} + diff --git a/doc/install-from-source.rst b/doc/install-from-source.rst index f4885a1ab88..64bdc79a3a5 100644 --- a/doc/install-from-source.rst +++ b/doc/install-from-source.rst @@ -2,294 +2,222 @@ Option 2: Install from Source ============================= -This section describes how to install CKAN from source. Whereas :doc:`install-from-package` requires Ubuntu 10.04, this way of installing CKAN is more flexible to work with other distributions and operating systems. Please share your experiences on our wiki: http://wiki.ckan.org/Install +This section describes how to install CKAN from source. Although +:doc:`install-from-package` is simpler, it requires Ubuntu 10.04. Installing +CKAN from source works with Ubuntu 10.04, with other versions of Ubuntu (e.g. +12.04) and with other operating systems (e.g. RedHat, Fedora, CentOS, OS X). If +you install CKAN from source on your own operating system, please share your +experiences on our wiki: http://wiki.ckan.org/Install -This is also the option to use if you are going to develop the CKAN source. +From source is also the right installation method for developers who want to +work on CKAN. -.. warning:: This option is more complex than :doc:`install-from-package`. +If you run into problems, see :doc:`common-error-messages` or contact `the +ckan-dev mailing list `_. -There is a page of help for dealing with :doc:`common-error-messages`. +1. Install the required packages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For support during installation, please contact `the ckan-dev mailing list `_. +If you're using a Debian-based operating system (such as Ubuntu) install the +required packages with this command:: -1. Ensure the required packages are installed -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + sudo apt-get install python-dev postgresql libpq-dev python-pip python-virtualenv git-core solr-jetty openjdk-6-jdk -If you have access to ``apt-get``, you can install these packages as follows: - -:: - - sudo apt-get install mercurial python-dev postgresql libpq-dev - sudo apt-get install libxml2-dev libxslt-dev python-virtualenv - sudo apt-get install wget build-essential git-core subversion - sudo apt-get install solr-jetty openjdk-6-jdk - -Otherwise, you should install these packages from source. +If you're not using a Debian-based operating system, find the best way to +install the following packages on your operating system (see +http://wiki.ckan.org/Install for help): ===================== =============================================== Package Description ===================== =============================================== -mercurial `Source control `_ -python `Python v2.5-2.7 `_ -postgresql `PostgreSQL database `_ -libpq `PostgreSQL library `_ -libxml2 `XML library development files `_ -libxslt `XSLT library development files `_ -virtualenv `Python virtual environments `_ -wget `Command line tool for downloading from the web `_ -build-essential Tools for building source code (or up-to-date Xcode on Mac) -git `Git source control (for getting MarkupSafe src) `_ -subversion `Subversion source control (for pyutilib) `_ -solr `Search engine `_ -jetty `HTTP server `_ (used for Solr) -openjdk-6-jdk `OpenJDK Java library `_ +Python `The Python programming language, v2.5-2.7 `_ +PostgreSQL `The PostgreSQL database system `_ +libpq `The C programmer's interface to PostgreSQL `_ +pip `A tool for installing and managing Python packages `_ +virtualenv `The virtual Python environment builder `_ +Git `A distributed version control system `_ +Apache Solr `A search platform `_ +Jetty `An HTTP server `_ (used for Solr) +OpenJDK 6 JDK `The Java Development Kit `_ ===================== =============================================== - -2. Create a Python virtual environment. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In your home directory run the command below. It is currently important to -call your virtual environment ``pyenv`` so that the automated deployment tools -work correctly. - -:: - - cd ~ - virtualenv pyenv - -.. tip :: - - If you don't have a ``python-virtualenv`` package in your distribution - you can get a ``virtualenv.py`` script from within the - `virtualenv source distribution `_ - and then run ``python virtualenv.py pyenv`` instead. - -To help with automatically installing CKAN dependencies we use a tool -called ``pip``. Make sure you have activated your environment (see step 3) -and then install it from an activated shell like this: - -:: - - easy_install pip -3. Activate your virtual environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +2. Install CKAN into a Python virtual environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To work with CKAN it is best to adjust your shell settings so that your -shell uses the virtual environment you just created. You can do this like -so: +a. Create a Python virtual environment (virtualenv) to install CKAN into (in + this example we create a virtualenv called ``pyenv`` in our home + directory), and activate it:: -:: - - . pyenv/bin/activate - -When your shell is activated you will see the prompt change to something -like this: - -:: - - (pyenv)[ckan@host ~/]$ - -An activated shell looks in your virtual environment first when choosing -which commands to run. If you enter ``python`` now it will actually -run ``~/pyenv/bin/python``, not the default ``/usr/bin/python`` which is what you want for CKAN. You can install python packages install this new environment and they won't affect the default ``/usr/bin/python``. This is necessary so you can use particular versions of python packages, rather than the ones installed with default paython, and these installs do not affect other python software on your system that may not be compatible with these packages. + virtualenv --no-site-packages ~/pyenv + . ~/pyenv/bin/activate -4. Install CKAN source code -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +b. Install the CKAN source code into your virtualenv. To install the latest + development version of CKAN (the most recent commit on the master branch of + the CKAN git repository), run:: -Here is how to install the latest code (HEAD on the master branch):: + pip install -e 'git+https://github.com/okfn/ckan.git#egg=ckan' - pip install --ignore-installed -e git+https://github.com/okfn/ckan.git#egg=ckan - -If you want to install a specific version, e.g. for v1.5.1:: - - pip install --ignore-installed -e git+https://github.com/okfn/ckan.git@ckan-1.7#egg=ckan - -5. Install Additional Dependencies -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -CKAN has a set of dependencies it requires which you should install too. These are listed in three text files: requires/lucid_*.txt, followed by WebOb explicitly. - -First we install two of the three lists of dependencies: - -:: + Alternatively, to install a specific version such as CKAN 1.7.1 run:: - pip install --ignore-installed -r pyenv/src/ckan/requires/lucid_missing.txt -r pyenv/src/ckan/requires/lucid_conflict.txt - pip install webob==1.0.8 + pip install -e 'git+https://github.com/okfn/ckan.git@ckan-1.7.1#egg=ckan' -The ``--ignore-installed`` option ensures ``pip`` installs software into -this virtual environment even if it is already present on the system. +c. Install the Python modules that CKAN requires into your virtualenv:: -WebOb has to be installed explicitly afterwards because by installing pylons with `--ignore-installed` you end up with a newer (incompatible) version than the one that Pylons and CKAN need. + pip install -r ~/pyenv/src/ckan/pip-requirements.txt -Now to install the remaining dependencies in requires/lucid_present.txt and you are using Ubuntu Lucid 10.04 you can install the system versions:: - - sudo apt-get install python-pybabel python-psycopg2 - sudo apt-get install python-pylons python-repoze.who - sudo apt-get install python-repoze.who-plugins python-tempita python-zope.interface - -Alternatively, if you are not using Ubuntu Lucid 10.04 you'll need to install them like this: - -:: - - pip install --ignore-installed -r pyenv/src/ckan/requires/lucid_present.txt - -At this point you will need to deactivate and then re-activate your -virtual environment to ensure that all the scripts point to the correct -locations: - -:: - - deactivate - . pyenv/bin/activate - -6. Setup a PostgreSQL database +3. Setup a PostgreSQL database ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -List existing databases: - - :: +List existing databases:: sudo -u postgres psql -l -Check that the encoding of databases is 'UTF8', if not -internationalisation may be a problem. Since changing the encoding of PostgreSQL -may mean deleting existing databases, it is suggested that this is fixed before -continuing with the CKAN install. +Check that the encoding of databases is 'UTF8', if not internationalisation may +be a problem. Since changing the encoding of PostgreSQL may mean deleting +existing databases, it is suggested that this is fixed before continuing with +the CKAN install. Next you'll need to create a database user if one doesn't already exist. - .. tip :: - - If you choose a database name, user or password which are different from the example values suggested below then you'll need to change the sqlalchemy.url value accordingly in the CKAN configuration file that you'll create in the next step. +.. tip :: -Here we create a user called ``ckanuser`` and will enter ``pass`` for the password when prompted: + If you choose a database name, user or password which are different from + the example values suggested below then you'll need to change the + sqlalchemy.url value accordingly in the CKAN configuration file that you'll + create in the next step. - :: +Create a user called ``ckanuser``, and enter ``pass`` for the password when +prompted:: sudo -u postgres createuser -S -D -R -P ckanuser -Now create the database (owned by ``ckanuser``), which we'll call ``ckantest``: - - :: +Create the database (owned by ``ckanuser``), which we'll call ``ckantest``:: sudo -u postgres createdb -O ckanuser ckantest -7. Create a CKAN config file +4. Create a CKAN config file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Make sure you are in an activated environment (see step 3) so that Python -Paste and other modules are put on the python path (your command prompt will -start with ``(pyenv)`` if you have) then change into the ``ckan`` directory -which will have been created when you installed CKAN in step 4 and create the -CKAN config file using Paste. These instructions call it ``development.ini`` since that is the required name for running the CKAN tests. But for a server deployment then you might want to call it say after the server hostname e.g. ``test.ckan.net.ini``. - - :: +With your virtualenv activated, change to the ckan directory and create a CKAN +config file:: - cd pyenv/src/ckan + cd ~/pyenv/src/ckan paster make-config ckan development.ini -If you used a different database name or password when creating the database -in step 6 you'll need to now edit ``development.ini`` and change the -``sqlalchemy.url`` line, filling in the database name, user and password you used. +.. tip :: + + If you used a different database name or password when creating the database in + step 6 you'll need to now edit ``development.ini`` and change the + ``sqlalchemy.url`` line, filling in the database name, user and password you + used:: - :: - - sqlalchemy.url = postgresql://ckanuser:pass@localhost/ckantest + sqlalchemy.url = postgresql://ckanuser:pass@localhost/ckantest -If you're using a remote host with password authentication rather than SSL authentication, use:: + If you're using a remote host with password authentication rather than SSL + authentication, use:: - sqlalchemy.url = postgresql://:@/ckan?sslmode=disable + sqlalchemy.url = postgresql://:@/ckan?sslmode=disable -.. caution :: +.. tip :: - Legacy installs of CKAN may have the config file in the pyenv directory, e.g. ``pyenv/ckan.net.ini``. This is fine but CKAN probably won't be able to find your ``who.ini`` file. To fix this edit ``pyenv/ckan.net.ini``, search for the line ``who.config_file = %(here)s/who.ini`` and change it to ``who.config_file = who.ini``. + Legacy installs of CKAN may have the config file in the pyenv directory, e.g. + ``pyenv/ckan.net.ini``. This is fine but CKAN probably won't be able to find + your ``who.ini`` file. To fix this edit ``pyenv/ckan.net.ini``, search for + the line ``who.config_file = %(here)s/who.ini`` and change it to + ``who.config_file = who.ini``. -8. Setup Solr +5. Setup Solr ~~~~~~~~~~~~~ -Set up Solr following the instructions on :ref:`solr-single` or :ref:`solr-multi-core` depending on your needs. - -Set appropriate values for the ``ckan.site_id`` and ``solr_url`` config variables in your CKAN config file: +Follow the instructions in :ref:`solr-single` or :ref:`solr-multi-core` to +setup Solr, set appropriate values for the ``ckan.site_id`` and ``solr_url`` +config variables in your CKAN config file: :: ckan.site_id=my_ckan_instance solr_url=http://127.0.0.1:8983/solr -9. Create database tables +6. Create database tables ~~~~~~~~~~~~~~~~~~~~~~~~~ -Now that you have a configuration file that has the correct settings for -your database, you'll need to create the tables. Make sure you are still in an +Now that you have a configuration file that has the correct settings for your +database, you'll need to create the tables. Make sure you are still in an activated environment with ``(pyenv)`` at the front of the command prompt and -then from the ``pyenv/src/ckan`` directory run this command. - -If your config file is called development.ini: - -:: +then from the ``~/pyenv/src/ckan`` directory run this command:: - paster --plugin=ckan db init + paster --plugin=ckan db init -or if your config file is something else, you need to specify it. e.g.:: +You should see ``Initialising DB: SUCCESS``. - paster --plugin=ckan db init --config=test.ckan.net.ini +.. tip :: -You should see ``Initialising DB: SUCCESS``. + If the command prompts for a password it is likely you haven't set up the + database configuration correctly in step 6. -If the command prompts for a password it is likely you haven't set up the -database configuration correctly in step 6. +.. tip :: -10. Create the cache and session directories -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + If your config file is not called ``development.ini`` you must give the + ``--config`` option, for example with a config file called + ``test.ckan.net.ini`` you would use:: -You need to create two directories for CKAN to put temporary files: - * Pylon's cache directory, specified by `cache_dir` in the config file. - * Repoze.who's OpenId session directory, specified by `store_file_path` in pyenv/ckan/who.ini + paster --plugin=ckan db init --config=test.ckan.net.ini -(from the ``pyenv/src/ckan`` directory or wherever your CKAN ini file you recently created is located): +7. Create the data and sstore directories +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :: +Create the ``data`` and ``sstore`` directories, in the same directory that +contains your CKAN config file (e.g. ``~/pyenv/src/ckan``):: mkdir data sstore +The location of the ``data`` directory, which CKAN uses as its Pylons cache, is +is specified by the ``cache_dir`` setting in your CKAN config file. -11. Link to who.ini -~~~~~~~~~~~~~~~~~~~ - -``who.ini`` (the Repoze.who configuration) needs to be accessible in the same directory as your CKAN config file. So if your config file is not in ``pyenv/src/ckan``, then cd to the directory with your config file and create a symbolic link to ``who.ini``. e.g.:: +The location of the ``sstore`` directory, which CKAN uses as its Repoze.who +OpenID session directory, is specified by the ``store_file_path`` setting in +the ``who.ini`` file. - ln -s pyenv/src/ckan/who.ini +8. Link to who.ini +~~~~~~~~~~~~~~~~~~ -12. Test the CKAN webserver -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``who.ini`` (the Repoze.who configuration file) needs to be accessible in the +same directory as your CKAN config file. So if your config file is not in +``~/pyenv/src/ckan``, then cd to the directory with your config file and create a +symbolic link to ``who.ini``. e.g.:: -You can use Paste to serve CKAN from the command-line. This is a simple and lightweight way to serve CKAN and is especially useful for testing. However a production deployment will probably want to be served using Apache or nginx - see :doc:`post-installation` + ln -s ~/pyenv/src/ckan/who.ini -.. note:: If you've started a new shell, you'll have to activate the environment again first - see step 3. +9. Run CKAN in the development web server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -(from the ``pyenv/src/ckan`` directory): +You can use the Paste development server to serve CKAN from the command-line. +This is a simple and lightweight way to serve CKAN that is useful for +development and testing. For production it's better to serve CKAN using +Apache or nginx (see :doc:`post-installation`). - :: +With your virtualenv activated, run this command from the ``~/pyenv/src/ckan`` +directory:: paster serve development.ini +Open http://127.0.0.1:5000/ in your web browser, and you should see the CKAN +front page. -13. Browse CKAN -~~~~~~~~~~~~~~~ +.. tip:: If you installed CKAN on a remote machine then you'll need to run + the web browser on that same machine. For example run the textual web browser + `w3m` in a separate ssh session to the one running `paster serve`. -Point your web browser at: http://127.0.0.1:5000/ +10. Run the CKAN Tests +~~~~~~~~~~~~~~~~~~~~~~ -The CKAN homepage should load. +Now that you've installed CKAN, you should run CKAN's tests to make sure that +they all pass. See :doc:`test`. -.. note:: if you installed CKAN on a remote machine then you will need to run the web browser on that same machine. For example run the textual web browser `w3m` in a separate ssh session to the one running `paster serve`. - -Finally, if doing development you should make sure that tests pass, as described in :ref:`basic-tests`. - -14. You are done +11. You're done! ~~~~~~~~~~~~~~~~ -You can now proceed to :doc:`post-installation` which covers getting an administrator account created and deploying using Apache. - +You can now proceed to :doc:`post-installation` which covers creating a CKAN +sysadmin account and deploying CKAN with Apache. diff --git a/doc/solr-setup.rst b/doc/solr-setup.rst index ffc044a080d..a280a69cb0c 100644 --- a/doc/solr-setup.rst +++ b/doc/solr-setup.rst @@ -57,9 +57,18 @@ and the admin site:: http://localhost:8983/solr/admin -.. note:: If you get the message ``Could not start Jetty servlet engine because no Java Development Kit (JDK) was found.`` then you will have to edit the ``JAVA_HOME`` setting in ``/etc/default/jetty`` (adjusting the path for your machine's JDK install): +.. note:: + + If you get the message ``Could not start Jetty servlet engine because no + Java Development Kit (JDK) was found.`` then you will have to edit the + ``JAVA_HOME`` setting in ``/etc/default/jetty`` to point to your machine's + JDK install location. For example:: + + JAVA_HOME=/usr/lib/jvm/java-6-openjdk-amd64/ + + or:: - ``JAVA_HOME=/usr/lib/jvm/java-6-openjdk-amd64/`` + JAVA_HOME=/usr/lib/jvm/java-6-openjdk-i386/ Now run:: @@ -77,7 +86,7 @@ so, create a symbolic link to the schema file in the config folder. Use the late supported by the CKAN version you are installing (it will generally be the highest one):: sudo mv /etc/solr/conf/schema.xml /etc/solr/conf/schema.xml.bak - sudo ln -s ~/ckan/ckan/config/solr/schema-1.4.xml /etc/solr/conf/schema.xml + sudo ln -s ~/pyenv/src/ckan/ckan/config/solr/schema-1.4.xml /etc/solr/conf/schema.xml Now restart jetty:: diff --git a/doc/test.rst b/doc/test.rst index d88ed047d0c..ce8169c91e9 100644 --- a/doc/test.rst +++ b/doc/test.rst @@ -12,31 +12,22 @@ Installing Additional Dependencies ---------------------------------- Some additional dependencies are needed to run the tests. Make sure you've -created a config file at ``pyenv/ckan/development.ini``, then activate your +created a config file at ``~/pyenv/ckan/development.ini``, then activate your virtual environment:: - . pyenv/bin/activate + . ~/pyenv/bin/activate Install nose and other test-specific CKAN dependencies into your virtual environment:: - pip install --ignore-installed -r pyenv/src/ckan/pip-requirements-test.txt - -At this point you'll need to deactivate and then re-activate your -virtual environment to ensure that all the scripts point to the correct -locations: - -:: - - deactivate - . pyenv/bin/activate + pip install -r ~/pyenv/src/ckan/pip-requirements-test.txt Testing with SQLite ------------------- To run the CKAN tests using SQLite as the database library:: - cd pyenv/src/ckan + cd ~/pyenv/src/ckan nosetests --ckan ckan You *must* run the tests from the CKAN directory as shown above, otherwise the @@ -123,7 +114,4 @@ is how the database is created and upgraded in production. Common error messages --------------------- -Often errors are due to set-up errors. Always refer to the CKAN buildbot as the -canonical build. - Consult :doc:`common-error-messages` for solutions to a range of setup problems. diff --git a/pip-requirements-test.txt b/pip-requirements-test.txt index 41a359c0264..b8103e124eb 100644 --- a/pip-requirements-test.txt +++ b/pip-requirements-test.txt @@ -1,5 +1,6 @@ # These are packages that required when running ckan tests nose +requests==0.6.4 -e git+https://github.com/okfn/ckanclient#egg=ckanclient diff --git a/pip-requirements.txt b/pip-requirements.txt index 9ff9d104906..8a9fc42ce87 100644 --- a/pip-requirements.txt +++ b/pip-requirements.txt @@ -1,15 +1,27 @@ -# This file allows you to install CKAN and all its dependencies -# in a virtual environment. -# -# Use it like this: -# -# pip install --ignore-installed -r pip-requirements.txt - --e git+https://github.com/okfn/ckan@master#egg=ckan -# CKAN dependencies --r https://github.com/okfn/ckan/raw/master/requires/lucid_conflict.txt --r https://github.com/okfn/ckan/raw/master/requires/lucid_present.txt --r https://github.com/okfn/ckan/raw/master/requires/lucid_missing.txt - -# NOTE: Developers, please do not edit this file. Changes should go in the -# appropriate files in the `requires' directory. +# This file lists CKAN's dependencies so that you can install them (e.g. into +# a Python virtual environment) after you have installed CKAN, with a command +# like: pip install -r pip-requirements.txt. See the Install from Source +# instructions in CKAN's documentation for full installation instructions. +Genshi==0.6 +sqlalchemy-migrate==0.7.1 +sqlalchemy==0.7.3 +webhelpers==1.2 +PyUtilib==4.0.2848 +-e git+https://github.com/okfn/vdm.git@vdm-0.11#egg=vdm +solrpy==0.9.4 +formalchemy==1.4.1 +pairtree==0.7.1-T +ofs==0.4.1 +apachemiddleware==0.1.1 +markupsafe==0.9.2 +babel==0.9.4 +psycopg2==2.0.13 +webob==1.0.8 +Pylons==0.9.7 +repoze.who==1.0.19 +tempita==0.4 +zope.interface==3.5.3 +repoze.who.plugins.openid==0.5.3 +repoze.who-friendlyform==1.0.8 +routes==1.12 +paste==1.7.2 diff --git a/requires/lucid_conflict.txt b/requires/lucid_conflict.txt deleted file mode 100644 index 2c6a3ca60f0..00000000000 --- a/requires/lucid_conflict.txt +++ /dev/null @@ -1,11 +0,0 @@ -# These are packages where we require a different version of a package from -# the one in Lucid. Rather than packaging a backport which could potentially -# interfere with a user's other installed software, we put these modules into -# a single location which CKAN imports from in preference to the Lucid -# equivalent. - -Genshi==0.6 -sqlalchemy-migrate==0.7.1 -sqlalchemy==0.7.3 -webhelpers==1.2 - diff --git a/requires/lucid_missing.txt b/requires/lucid_missing.txt deleted file mode 100644 index d6658f30ea1..00000000000 --- a/requires/lucid_missing.txt +++ /dev/null @@ -1,18 +0,0 @@ -# These are packages that we rely on that aren't present in Lucid. We package -# them and put them in our own CKAN repository - -# Packages we install from source (could perhaps use release versions) -# pyutilib.component.core>=4.1,<4.1.99 --e svn+https://software.sandia.gov/svn/public/pyutilib/pyutilib.component.core/trunk@1972#egg=pyutilib.component.core -# vdm>=0.10,<0.11.99 --e git+https://github.com/okfn/vdm.git@vdm-0.11#egg=vdm - -# Packages already on pypi.python.org -solrpy==0.9.4 -formalchemy==1.4.1 -pairtree==0.7.1-T -ofs==0.4.1 - -apachemiddleware==0.1.1 -# markupsafe is required by webhelpers==1.2 required by formalchemy with SQLAlchemy 0.6 -markupsafe==0.9.2 diff --git a/requires/lucid_present.txt b/requires/lucid_present.txt deleted file mode 100644 index a4cbfb03f12..00000000000 --- a/requires/lucid_present.txt +++ /dev/null @@ -1,29 +0,0 @@ -# These CKAN dependencies are already in Lucid and should be installed via -# apt-get if you are on that platform. If you are using a different platform -# you can install these dependencies via pip instead. -# -# sudo apt-get install python-pybabel python-psycopg2 -# sudo apt-get install python-pylons python-repoze.who -# sudo apt-get install python-repoze.who-plugins python-tempita python-zope.interface - -babel==0.9.4 -psycopg2==2.0.13 -# Specifying particular version of WebOb because later version has incompatibility -# with pylons 0.9.7 (change to imports of Multidict) -webob==1.0.8 -Pylons==0.9.7 -repoze.who==1.0.19 -tempita==0.4 -zope.interface==3.5.3 -# These are both combined into the python-repoze.who-plugins package -repoze.who.plugins.openid==0.5.3 -# Actually from python-repoze.who-plugins but the openid plugin in the same -# package is too old -repoze.who-friendlyform==1.0.8 -routes==1.11 -paste==1.7.2 -pastescript==1.7.3 - -# NOTE: Developers, please do not edit the versions, these versions are fixed -# in Lucid. If you start to depend on a different vesion you'll need to -# remove the version here and package your version as a conflict.