diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index a8d2ff23da5..33c06fcbb91 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -2542,10 +2542,12 @@ def package_activity_list(context, data_dict): offset = int(data_dict.get('offset', 0)) limit = data_dict['limit'] # defaulted, limited & made an int by schema + activity_type = data_dict.get('activity_type') activity_objects = model.activity.package_activity_list( package.id, limit=limit, offset=offset, include_hidden_activity=include_hidden_activity, + activity_type=activity_type, ) return model_dictize.activity_list_dictize( diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index 4f4ad66d2ea..2d95c3ae695 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -611,7 +611,8 @@ def default_dashboard_activity_list_schema( def default_activity_list_schema( not_missing, unicode_safe, configured_default, natural_number_validator, limit_to_configured_maximum, - ignore_missing, boolean_validator, ignore_not_sysadmin): + ignore_missing, boolean_validator, ignore_not_sysadmin, + activity_type_exists): schema = default_pagination_schema() schema['id'] = [not_missing, unicode_safe] schema['limit'] = [ @@ -620,6 +621,9 @@ def default_activity_list_schema( limit_to_configured_maximum('ckan.activity_list_limit_max', 100)] schema['include_hidden_activity'] = [ ignore_missing, ignore_not_sysadmin, boolean_validator] + schema['activity_type'] = [ + ignore_missing, activity_type_exists + ] return schema diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index fdc0820e946..4ff4464da5b 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -297,24 +297,36 @@ def activity_type_exists(activity_type): raise Invalid('%s: %s' % (_('Not found'), _('Activity type'))) +VALIDATORS_PACKAGE_ACTIVITY_TYPES = { + 'new package': package_id_exists, + 'changed package': package_id_exists, + 'deleted package': package_id_exists, + 'follow dataset': package_id_exists, +} + +VALIDATORS_USER_ACTIVITY_TYPES = { + 'new user': user_id_exists, + 'changed user': user_id_exists, + 'follow user': user_id_exists, +} + +VALIDATORS_GROUP_ACTIVITY_TYPES = { + 'new group': group_id_exists, + 'changed group': group_id_exists, + 'deleted group': group_id_exists, + 'new organization': group_id_exists, + 'changed organization': group_id_exists, + 'deleted organization': group_id_exists, + 'follow group': group_id_exists, +} + # A dictionary mapping activity_type values from activity dicts to functions # for validating the object_id values from those same activity dicts. -object_id_validators = { - '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, - 'new organization' : group_id_exists, - 'changed organization' : group_id_exists, - 'deleted organization' : group_id_exists, - 'follow group' : group_id_exists, - } +# Merge dicts (Python 2 compatible) +object_id_validators = dict(**VALIDATORS_PACKAGE_ACTIVITY_TYPES) +object_id_validators.update(**VALIDATORS_USER_ACTIVITY_TYPES) +object_id_validators.update(**VALIDATORS_GROUP_ACTIVITY_TYPES) + def object_id_validator(key, activity_dict, errors, context): '''Validate the 'object_id' value of an activity_dict. diff --git a/ckan/model/activity.py b/ckan/model/activity.py index 12ac4b9c631..c88dfe5f453 100644 --- a/ckan/model/activity.py +++ b/ckan/model/activity.py @@ -182,7 +182,10 @@ def _package_activity_query(package_id): def package_activity_list( - package_id, limit, offset, include_hidden_activity=False): + package_id, limit, offset, + include_hidden_activity=False, + activity_type=None, + ): '''Return the given dataset (package)'s public activity stream. Returns all activities about the given dataset, i.e. where the given @@ -198,6 +201,9 @@ def package_activity_list( if not include_hidden_activity: q = _filter_activitites_from_users(q) + if activity_type: + q = _filter_activitites_by_type(q, activity_type) + return _activities_at_offset(q, limit, offset) @@ -450,9 +456,21 @@ def recently_changed_packages_activity_list(limit, offset): return _activities_at_offset(q, limit, offset) +def _filter_activitites_by_type(q, activity_type): + ''' + Adds a filter to an existing query object to + only show one activity type + ''' + users_to_avoid = _activity_stream_get_filtered_users() + if users_to_avoid: + q = q.filter(ckan.model.Activity.activity_type==activity_type) + + return q + + def _filter_activitites_from_users(q): ''' - Adds a filter to an existing query object ot avoid activities from users + Adds a filter to an existing query object to avoid activities from users defined in :ref:`ckan.hide_activity_from_users` (defaults to the site user) ''' users_to_avoid = _activity_stream_get_filtered_users() diff --git a/ckan/public/base/javascript/modules/activity-stream.js b/ckan/public/base/javascript/modules/activity-stream.js index 16314f5a661..69874ba0b44 100644 --- a/ckan/public/base/javascript/modules/activity-stream.js +++ b/ckan/public/base/javascript/modules/activity-stream.js @@ -1,119 +1,34 @@ /* Activity stream - * Handle the loading more of activity items within actiivity streams + * Handle the pagination for activity list * * Options - * - more: are there more items to load - * - context: what's the context for the ajax calls - * - id: what's the id of the context? - * - offset: what's the current offset? + * - page: current page number */ this.ckan.module('activity-stream', function($) { return { - /* options object can be extended using data-module-* attributes */ - options : { - more: null, - id: null, - context: null, - offset: null, - loading: false - }, - + /* Initialises the module setting up elements and event listeners. * * Returns nothing. */ initialize: function () { - $.proxyAll(this, /_on/); - var options = this.options; - options.more = (options.more == 'True'); - this._onBuildLoadMore(); - $(window).on('scroll', this._onScrollIntoView); - this._onScrollIntoView(); - }, - - /* Function that tells if el is within the window viewpost - * - * Returns boolean - */ - elementInViewport: function(el) { - var top = el.offsetTop; - var left = el.offsetLeft; - var width = el.offsetWidth; - var height = el.offsetHeight; - while(el.offsetParent) { - el = el.offsetParent; - top += el.offsetTop; - left += el.offsetLeft; - } - return ( - top < (window.pageYOffset + window.innerHeight) && - left < (window.pageXOffset + window.innerWidth) && - (top + height) > window.pageYOffset && - (left + width) > window.pageXOffset + $('#activity_types_filter_select').on( + 'change', + this._onChangeActivityType ); }, - /* Whenever the window scrolls check if the load more button - * exists, if it's in the view and we're not already loading. - * If all conditions are satisfied... fire a click event on - * the load more button. - * - * Returns nothing - */ - _onScrollIntoView: function() { - var el = $('.load-more a', this.el); - if (el.length == 1) { - var in_viewport = this.elementInViewport(el[0]); - if (in_viewport && !this.options.loading) { - el.trigger('click'); - } - } - }, - - /* If we are able to load more... then attach the ajax request - * to the load more button. - * - * Returns nothing - */ - _onBuildLoadMore: function() { - var options = this.options; - if (options.more) { - $('.load-more', this.el).on('click', 'a', this._onLoadMoreClick); - options.offset = $('.item', this.el).length; - } - }, - - /* Fires when someone clicks the load more button - * ... and if not loading then make the API call to load - * more activities + + /* Filter using the selected + * activity type * * Returns nothing */ - _onLoadMoreClick: function (event) { - event.preventDefault(); - var options = this.options; - if (!options.loading) { - options.loading = true; - $('.load-more a', this.el).html(this._('Loading...')).addClass('disabled'); - this.sandbox.client.call('GET', options.context+'_activity_list_html', '?id='+options.id+'&offset='+options.offset, this._onActivitiesLoaded); - } + _onChangeActivityType: function (event) { + // event.preventDefault(); + url = $("#activity_types_filter_select option:selected" ).data('url'); + window.location = url; }, - /* Callback for after the API call - * - * Returns nothing - */ - _onActivitiesLoaded: function(json) { - var options = this.options; - var result = $(json.result); - options.more = ( result.data('module-more') == 'True' ); - options.offset += 30; - $('.load-less', result).remove(); - $('.load-more', this.el).remove(); - $('li', result).appendTo(this.el); - this._onBuildLoadMore(); - options.loading = false; - } - }; }); diff --git a/ckan/templates/activity_streams/activity_stream_items.html b/ckan/templates/activity_streams/activity_stream_items.html deleted file mode 100644 index 506eb16e6f8..00000000000 --- a/ckan/templates/activity_streams/activity_stream_items.html +++ /dev/null @@ -1,26 +0,0 @@ -{% set has_more_length = g.activity_list_limit|int %} -{% set has_more = activities|length > has_more_length %} - -{% block activity_stream %} - {% if activities %} -
{{ _('No activities are within this activity stream') }}
- {% endblock %} - {% endif %} -{% endblock %} diff --git a/ckan/templates/package/activity.html b/ckan/templates/package/activity.html index c83e7b62ec0..923afb382ca 100644 --- a/ckan/templates/package/activity.html +++ b/ckan/templates/package/activity.html @@ -8,5 +8,68 @@+ {{ _('No activity found') }} + {% if activity_type %} + {{ _('for this type') }}. + {% endif %} +
+ {% endif %} + + {# allow extensions to continue using this template without new vars #} + {% if page is defined %} + + + + {% endif %} + {% endblock %} diff --git a/ckan/views/dataset.py b/ckan/views/dataset.py index e057dcaab93..45fe0379752 100644 --- a/ckan/views/dataset.py +++ b/ckan/views/dataset.py @@ -1,5 +1,6 @@ # encoding: utf-8 import logging +import math from collections import OrderedDict from functools import partial from six.moves.urllib.parse import urlencode @@ -21,6 +22,7 @@ import ckan.plugins as plugins import ckan.authz as authz from ckan.common import _, config, g, request +from ckan.logic.validators import VALIDATORS_PACKAGE_ACTIVITY_TYPES from ckan.views.home import CACHE_PARAMETERS from ckan.lib.plugins import lookup_package_plugin from ckan.lib.render import TemplateNotFound @@ -1077,7 +1079,7 @@ def get(self, package_type, id): ) -def activity(package_type, id): +def activity(package_type, id, page=0, activity_type=None): """Render this package's public activity stream page. """ context = { @@ -1088,21 +1090,39 @@ def activity(package_type, id): u'auth_user_obj': g.userobj } data_dict = {u'id': id} + base_limit = int(config.get(u'ckan.activity_list_limit', 30)) + max_limit = int(config.get(u'ckan.activity_list_limit_max', 100)) + limit = min(base_limit, max_limit) + page = int(page) + offset = page * limit + try: pkg_dict = get_action(u'package_show')(context, data_dict) + activity_dict = { + u'id': pkg_dict[u'id'], + u'offset': offset, + u'limit': limit, + u'activity_type': activity_type + } pkg = context[u'package'] package_activity_stream = get_action( u'package_activity_list')( - context, {u'id': pkg_dict[u'id']}) + context, + activity_dict, + ) dataset_type = pkg_dict[u'type'] or u'dataset' except NotFound: return base.abort(404, _(u'Dataset not found')) except NotAuthorized: return base.abort(403, _(u'Unauthorized to read dataset %s') % id) - # TODO: remove - g.pkg_dict = pkg_dict - g.pkg = pkg + all_activities = model.activity.package_activity_list( + pkg_dict[u'id'], limit=0, offset=0, activity_type=activity_type + ) + has_more = len(all_activities) > offset + limit + total_pages = int(math.ceil(len(all_activities) / limit)) + + activity_types = VALIDATORS_PACKAGE_ACTIVITY_TYPES.keys() return base.render( u'package/activity.html', { @@ -1110,7 +1130,13 @@ def activity(package_type, id): u'pkg_dict': pkg_dict, u'pkg': pkg, u'activity_stream': package_activity_stream, - u'id': id, # i.e. package's current name + u'id': id, # i.e. package's current name, + u'limit': limit, + u'page': page, + u'has_more': has_more, + u'total_pages': total_pages, + u'activity_type': activity_type, + u'activity_types': activity_types, } ) @@ -1386,6 +1412,11 @@ def register_dataset_plugin_rules(blueprint): u'/groups/