diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 57395125e2c..4e9e2ea5585 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -628,9 +628,8 @@ def activity(self, id, offset=0): # Add the group's activity stream (already rendered to HTML) to the # template context for the group/read.html template to retrieve later. - c.group_activity_stream = \ - get_action('group_activity_list_html')(context, - {'id': c.group_dict['id'], 'offset': offset}) + c.group_activity_stream = get_action('group_activity_list_html')( + context, {'id': c.group_dict['id'], 'offset': offset}) return render('group/activity_stream.html') diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py index ca5f4899577..b0e13dccd2c 100644 --- a/ckan/controllers/user.py +++ b/ckan/controllers/user.py @@ -503,13 +503,74 @@ def activity(self, id, offset=0): return render('user/activity_stream.html') + def _get_dashboard_context(self, filter_type=None, filter_id=None, + q=None): + '''Return a dict needed by the dashboard view to determine context.''' + + def display_name(followee): + '''Return a display name for a user, group or dataset dict.''' + display_name = followee.get('display_name') + fullname = followee.get('fullname') + title = followee.get('title') + name = followee.get('name') + return display_name or fullname or title or name + + if (filter_type and filter_id): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'for_view': True} + data_dict = {'id': filter_id} + followee = None + + action_functions = { + 'dataset': 'package_show', + 'user': 'user_show', + 'group': 'group_show' + } + action_function = logic.get_action(action_functions.get(filter_type)) + # Is this a valid type? + if action_function is None: + raise abort(404, _('Follow item not found')) + try: + followee = action_function(context, data_dict) + except NotFound: + abort(404, _('{0} not found').format(filter_type)) + except NotAuthorized: + abort(401, _('Unauthorized to read {0} {1}').format( + filter_type, id)) + + if followee is not None: + return { + 'filter_type': filter_type, + 'q': q, + 'context': display_name(followee), + 'selected_id': followee.get('id'), + 'dict': followee, + } + + return { + 'filter_type': filter_type, + 'q': q, + 'context': _('Everything'), + 'selected_id': False, + 'dict': None, + } + def dashboard(self, id=None, offset=0): context = {'model': model, 'session': model.Session, 'user': c.user or c.author, 'for_view': True} data_dict = {'id': id, 'user_obj': c.userobj, 'offset': offset} self._setup_template_variables(context, data_dict) - c.dashboard_activity_stream = h.dashboard_activity_stream(id, offset) + q = request.params.get('q', u'') + filter_type = request.params.get('type', u'') + filter_id = request.params.get('name', u'') + + c.followee_list = get_action('followee_list')( + context, {'id': c.userobj.id, 'q': q}) + c.dashboard_activity_stream_context = self._get_dashboard_context( + filter_type, filter_id, q) + c.dashboard_activity_stream = h.dashboard_activity_stream( + id, filter_type, filter_id, offset) # Mark the user's new activities as old whenever they view their # dashboard page. diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index 490b5601fc5..a50edb569b6 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -220,6 +220,7 @@ def package_dictize(pkg, context): q = q.where(resource_group.c.package_id == pkg.id) result = _execute_with_revision(q, res_rev, context) result_dict["resources"] = resource_list_dictize(result, context) + result_dict['num_resources'] = len(result_dict.get('resources', [])) #tags tag_rev = model.package_tag_revision_table @@ -229,6 +230,7 @@ def package_dictize(pkg, context): ).where(tag_rev.c.package_id == pkg.id) result = _execute_with_revision(q, tag_rev, context) result_dict["tags"] = d.obj_list_dictize(result, context, lambda x: x["name"]) + result_dict['num_tags'] = len(result_dict.get('tags', [])) # Add display_names to tags. At first a tag's display_name is just the # same as its name, but the display_name might get changed later (e.g. diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 07eb5dd0565..c4ac5a4ba8e 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -1257,21 +1257,37 @@ def user_in_org_or_group(group_id): return len(query.all()) != 0 -def dashboard_activity_stream(user_id, offset=0): +def dashboard_activity_stream(user_id, filter_type=None, filter_id=None, + offset=0): '''Return the dashboard activity stream of the given user. :param user_id: the id of the user :type user_id: string + :param filter_type: the type of thing to filter by + :type filter_type: string + + :param filter_id: the id of item to filter by + :type filter_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, - 'offset': offset}) + + if filter_type: + action_functions = { + 'dataset': 'package_activity_list_html', + 'user': 'user_activity_list_html', + 'group': 'group_activity_list_html' + } + action_function = logic.get_action(action_functions.get(filter_type)) + return action_function(context, {'id': filter_id, 'offset': offset}) + else: + return logic.get_action('dashboard_activity_list_html')( + context, {'id': user_id, 'offset': offset}) def recently_changed_packages_activity_stream(): diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 01e9095a2ba..e5bbb97d117 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1861,12 +1861,17 @@ def user_activity_list(context, data_dict): _check_access('user_show', context, data_dict) model = context['model'] - user_id = _get_or_bust(data_dict, 'id') + + user_ref = _get_or_bust(data_dict, 'id') # May be user name or id. + user = model.User.get(user_ref) + if user is None: + raise logic.NotFound + offset = int(data_dict.get('offset', 0)) limit = int( data_dict.get('limit', config.get('ckan.activity_list_limit', 31))) - activity_objects = model.activity.user_activity_list(user_id, limit=limit, + activity_objects = model.activity.user_activity_list(user.id, limit=limit, offset=offset) return model_dictize.activity_list_dictize(activity_objects, context) @@ -1893,12 +1898,17 @@ def package_activity_list(context, data_dict): _check_access('package_show', context, data_dict) model = context['model'] - package_id = _get_or_bust(data_dict, 'id') + + package_ref = _get_or_bust(data_dict, 'id') # May be name or ID. + package = model.Package.get(package_ref) + if package is None: + raise logic.NotFound + offset = int(data_dict.get('offset', 0)) limit = int( data_dict.get('limit', config.get('ckan.activity_list_limit', 31))) - activity_objects = model.activity.package_activity_list(package_id, + activity_objects = model.activity.package_activity_list(package.id, limit=limit, offset=offset) return model_dictize.activity_list_dictize(activity_objects, context) @@ -1947,19 +1957,22 @@ def organization_activity_list(context, data_dict): :rtype: list of dictionaries ''' - # FIXME This is a duplicate of group_activity_list and - # package_activity_list but they claim to get the group/package by name - # or id but I think that they only get by id. Either they need fixing - # and remain seperate or they should share code - probably should share - # code anyway. + # FIXME: Filter out activities whose subject or object the user is not + # authorized to read. + _check_access('organization_show', context, data_dict) model = context['model'] - group_id = _get_or_bust(data_dict, 'id') - query = model.Session.query(model.Activity) - query = query.filter_by(object_id=group_id) - query = query.order_by(_desc(model.Activity.timestamp)) - query = query.limit(15) - activity_objects = query.all() + org_id = _get_or_bust(data_dict, 'id') + offset = int(data_dict.get('offset', 0)) + limit = int( + data_dict.get('limit', config.get('ckan.activity_list_limit', 31))) + + # Convert org_id (could be id or name) into id. + org_show = logic.get_action('organization_show') + org_id = org_show(context, {'id': org_id})['id'] + + activity_objects = model.activity.group_activity_list(org_id, + limit=limit, offset=offset) return model_dictize.activity_list_dictize(activity_objects, context) def recently_changed_packages_activity_list(context, data_dict): @@ -2107,7 +2120,6 @@ def organization_activity_list_html(context, data_dict): :rtype: string ''' - # FIXME does this work with a name? same issue with package/group activity_stream = organization_activity_list(context, data_dict) return activity_streams.activity_list_to_html(context, activity_stream) @@ -2319,14 +2331,43 @@ def am_following_group(context, data_dict): def _followee_count(context, data_dict, FollowerClass): - schema = context.get('schema', - ckan.logic.schema.default_follow_user_schema()) - data_dict, errors = _validate(data_dict, schema, context) - if errors: - raise ValidationError(errors) + if not context.get('skip_validation'): + schema = context.get('schema', + ckan.logic.schema.default_follow_user_schema()) + data_dict, errors = _validate(data_dict, schema, context) + if errors: + raise ValidationError(errors) return FollowerClass.followee_count(data_dict['id']) +def followee_count(context, data_dict): + '''Return the number of objects that are followed by the given user. + + Counts all objects, of any type, that the given user is following + (e.g. followed users, followed datasets, followed groups). + + :param id: the id of the user + :type id: string + + :rtype: int + + ''' + model = context['model'] + followee_users = _followee_count(context, data_dict, + model.UserFollowingUser) + + # followee_users has validated data_dict so the following functions don't + # need to validate it again. + context['skip_validation'] = True + + followee_datasets = _followee_count(context, data_dict, + model.UserFollowingDataset) + followee_groups = _followee_count(context, data_dict, + model.UserFollowingGroup) + + return sum((followee_users, followee_datasets, followee_groups)) + + def user_followee_count(context, data_dict): '''Return the number of users that are followed by the given user. @@ -2366,22 +2407,91 @@ def group_followee_count(context, data_dict): context['model'].UserFollowingGroup) -def user_followee_list(context, data_dict): - '''Return the list of users that are followed by the given user. +def followee_list(context, data_dict): + '''Return the list of objects that are followed by the given user. + + Returns all objects, of any type, that the given user is following + (e.g. followed users, followed datasets, followed groups.. ). :param id: the id of the user :type id: string - :rtype: list of dictionaries + :param q: a query string to limit results by, only objects whose display + name begins with the given string (case-insensitive) wil be returned + (optional) + :type q: string + + :rtype: list of dictionaries, each with keys 'type' (e.g. 'user', + 'dataset' or 'group'), 'display_name' (e.g. a user's display name, + or a package's title) and 'dict' (e.g. a dict representing the + followed user, package or group, the same as the dict that would be + returned by user_show, package_show or group_show) ''' - _check_access('user_followee_list', context, data_dict) + _check_access('followee_list', context, data_dict) 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) + def display_name(followee): + '''Return a display name for the given user, group or dataset dict.''' + display_name = followee.get('display_name') + fullname = followee.get('fullname') + title = followee.get('title') + name = followee.get('name') + return display_name or fullname or title or name + + # Get the followed objects. + # TODO: Catch exceptions raised by these *_followee_list() functions? + followee_dicts = [] + context['skip_validation'] = True + context['skip_authorization'] = True + for followee_list_function, followee_type in ( + (user_followee_list, 'user'), + (dataset_followee_list, 'dataset'), + (group_followee_list, 'group')): + dicts = followee_list_function(context, data_dict) + for d in dicts: + followee_dicts.append( + {'type': followee_type, + 'display_name': display_name(d), + 'dict': d}) + + followee_dicts.sort(key=lambda d: d['display_name']) + + q = data_dict.get('q') + if q: + q = q.strip().lower() + matching_followee_dicts = [] + for followee_dict in followee_dicts: + if followee_dict['display_name'].strip().lower().startswith(q): + matching_followee_dicts.append(followee_dict) + followee_dicts = matching_followee_dicts + + return followee_dicts + + +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 + + ''' + if not context.get('skip_authorization'): + _check_access('user_followee_list', context, data_dict) + + if not context.get('skip_validation'): + 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) + # Get the list of Follower objects. model = context['model'] user_id = data_dict.get('id') @@ -2403,12 +2513,15 @@ def dataset_followee_list(context, data_dict): :rtype: list of dictionaries ''' - _check_access('dataset_followee_list', context, data_dict) - 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) + if not context.get('skip_authorization'): + _check_access('dataset_followee_list', context, data_dict) + + if not context.get('skip_validation'): + 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) # Get the list of Follower objects. model = context['model'] @@ -2432,12 +2545,15 @@ def group_followee_list(context, data_dict): :rtype: list of dictionaries ''' - _check_access('group_followee_list', context, data_dict) - schema = context.get('schema', - ckan.logic.schema.default_follow_user_schema()) - data_dict, errors = _validate(data_dict, schema, context) - if errors: - raise ValidationError(errors) + if not context.get('skip_authorization'): + _check_access('group_followee_list', context, data_dict) + + if not context.get('skip_validation'): + schema = context.get('schema', + ckan.logic.schema.default_follow_user_schema()) + data_dict, errors = _validate(data_dict, schema, context) + if errors: + raise ValidationError(errors) # Get the list of UserFollowingGroup objects. model = context['model'] diff --git a/ckan/logic/auth/get.py b/ckan/logic/auth/get.py index c6412bc27c9..3df5689c2ea 100644 --- a/ckan/logic/auth/get.py +++ b/ckan/logic/auth/get.py @@ -248,6 +248,10 @@ def _followee_list(context, data_dict): return sysadmin(context, data_dict) +def followee_list(context, data_dict): + return _followee_list(context, data_dict) + + def user_followee_list(context, data_dict): return _followee_list(context, data_dict) diff --git a/ckan/model/activity.py b/ckan/model/activity.py index 73d5779622d..5ab31a27514 100644 --- a/ckan/model/activity.py +++ b/ckan/model/activity.py @@ -186,20 +186,32 @@ def group_activity_list(group_id, limit, offset): def _activites_from_users_followed_by_user_query(user_id): '''Return a query for all activities from users that user_id follows.''' import ckan.model as model - q = model.Session.query(model.Activity) - q = q.join(model.UserFollowingUser, - model.UserFollowingUser.object_id == model.Activity.user_id) - q = q.filter(model.UserFollowingUser.follower_id == user_id) + + # Get a list of the users that the given user is following. + follower_objects = model.UserFollowingUser.followee_list(user_id) + if not follower_objects: + # Return a query with no results. + return model.Session.query(model.Activity).filter("0=1") + + q = _user_activity_query(follower_objects[0].object_id) + q = q.union_all(*[_user_activity_query(follower.object_id) + for follower in follower_objects[1:]]) return q def _activities_from_datasets_followed_by_user_query(user_id): '''Return a query for all activities from datasets that user_id follows.''' import ckan.model as model - q = model.Session.query(model.Activity) - q = q.join(model.UserFollowingDataset, - model.UserFollowingDataset.object_id == model.Activity.object_id) - q = q.filter(model.UserFollowingDataset.follower_id == user_id) + + # Get a list of the datasets that the user is following. + follower_objects = model.UserFollowingDataset.followee_list(user_id) + if not follower_objects: + # Return a query with no results. + return model.Session.query(model.Activity).filter("0=1") + + q = _package_activity_query(follower_objects[0].object_id) + q = q.union_all(*[_package_activity_query(follower.object_id) + for follower in follower_objects[1:]]) return q diff --git a/ckan/public/base/css/main.css b/ckan/public/base/css/main.css index eaa904d3dac..66aab31b86b 100644 --- a/ckan/public/base/css/main.css +++ b/ckan/public/base/css/main.css @@ -6990,6 +6990,9 @@ header.masthead .account.not-authed { header.masthead .dropdown-menu { margin-top: -1px; } +header.masthead .user-dropdown-menu { + min-width: 120px; +} header.masthead .user-dropdown-menu a { color: #005d7a; } @@ -7436,9 +7439,175 @@ header.masthead .debug { -moz-border-radius: 100px; border-radius: 100px; } -.module-my-datasets .empty { +.dashboard .module-content { + *zoom: 1; + background: transparent url("../../../base/images/full-width-nav-right.png") 100% 0 repeat-y; +} +.dashboard .module-content:before, +.dashboard .module-content:after { + display: table; + content: ""; +} +.dashboard .module-content:after { + clear: both; +} +.dashboard .dashboard-main { + float: left; + width: 630px; + margin-right: 20px; +} +.dashboard .dashboard-aside { + float: left; + width: 220px; + padding-left: 20px; +} +.dashboard .dashboard-aside .nav-list { + margin: 10px -25px 0 -20px; +} +.dashboard .dashboard-aside .nav-list li a { + padding: 5px 20px; +} +#followee-filter .btn { + *zoom: 1; +} +#followee-filter .btn:before, +#followee-filter .btn:after { + display: table; + content: ""; +} +#followee-filter .btn:after { + clear: both; +} +#followee-filter .btn span, +#followee-filter .btn strong { + display: block; + float: left; + line-height: 1.5; +} +#followee-filter .btn span { + font-weight: normal; +} +#followee-filter .btn strong { + margin: 0 5px; + white-space: nowrap; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; +} +.dashboard-aside-context { + position: relative; + margin: -20px -25px 20px -20px; + padding: 20px; + border-left: 1px solid #DCDCDC; + border-bottom: 1px solid #DCDCDC; + background-color: #f5f5f5; + -webkit-border-radius: 0 5px 0 0; + -moz-border-radius: 0 5px 0 0; + border-radius: 0 5px 0 0; +} +.dashboard-aside-context h2 { + margin-bottom: 10px; +} +.dashboard-aside-context .arrow { + position: absolute; + content: ' '; + top: 24px; + left: -10px; + width: 10px; + height: 21px; + background: transparent url("../../../base/images/dashboard-followee-related.png"); +} +.popover-followee .popover-title { + display: none; +} +.popover-followee .popover-content { + padding: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.popover-followee .empty { padding: 10px; } +.popover-followee .popover-header { + *zoom: 1; + background-color: whiteSmoke; + padding: 5px; + border-bottom: 1px solid #cccccc; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.popover-followee .popover-header:before, +.popover-followee .popover-header:after { + display: table; + content: ""; +} +.popover-followee .popover-header:after { + clear: both; +} +.popover-followee .popover-header .add-on, +.popover-followee .popover-header input { + float: left; + margin: 0; +} +.popover-followee .popover-header .add-on { + padding: 4px 8px 4px 12px; + border-right-width: 0; + -webkit-border-radius: 100px 0 0 100px; + -moz-border-radius: 100px 0 0 100px; + border-radius: 100px 0 0 100px; +} +.popover-followee .popover-header input { + padding: 4px 12px 4px 8px; + font-size: 13px; + width: 211px; + -webkit-border-radius: 0 100px 100px 0; + -moz-border-radius: 0 100px 100px 0; + border-radius: 0 100px 100px 0; +} +.popover-followee .nav { + padding: 0; + max-height: 205px; + overflow: auto; + -webkit-border-radius: 0 0 3px 3px; + -moz-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; +} +.popover-followee .nav li a { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 7px 10px 7px 15px; + margin: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.popover-followee .nav li a i { + background-color: #187794; + color: #ffffff; + margin-right: 11px; + padding: 3px 5px; + line-height: 1; + -webkit-border-radius: 100px; + -moz-border-radius: 100px; + border-radius: 100px; + -webkit-box-shadow: inset 0 1px 2 x rgba(0, 0, 0, 0.2); + -moz-box-shadow: inset 0 1px 2 x rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 2 x rgba(0, 0, 0, 0.2); +} +.popover-followee .nav li a:hover i { + background-color: #000; +} +.popover-followee .nav li.active a i { + color: #187794; + background-color: #ffffff; +} +.ie8 .dashboard-aside { + width: 218px; +} .dropdown:hover .dropdown-menu { display: block; } diff --git a/ckan/public/base/images/dashboard-followee-related.png b/ckan/public/base/images/dashboard-followee-related.png new file mode 100644 index 00000000000..81374b09273 Binary files /dev/null and b/ckan/public/base/images/dashboard-followee-related.png differ diff --git a/ckan/public/base/images/full-width-nav-right.png b/ckan/public/base/images/full-width-nav-right.png new file mode 100644 index 00000000000..344b92cb458 Binary files /dev/null and b/ckan/public/base/images/full-width-nav-right.png differ diff --git a/ckan/public/base/javascript/modules/dashboard.js b/ckan/public/base/javascript/modules/dashboard.js index 7c30c655c6c..263b3467ec9 100644 --- a/ckan/public/base/javascript/modules/dashboard.js +++ b/ckan/public/base/javascript/modules/dashboard.js @@ -1,11 +1,93 @@ +/* User Dashboard + * Handles the filter dropdown menu and the reduction of the notifications number + * within the header to zero + * + * Examples + * + *
+ * + */ this.ckan.module('dashboard', function ($, _) { - return { - initialize: function () { - if ($('.new', this.el)) { - setTimeout(function() { - $('.masthead .notifications').removeClass('notifications-important').html('0'); - }, 1000); - } - } - }; + return { + button: null, + popover: null, + searchTimeout: null, + + /* Initialises the module setting up elements and event listeners. + * + * Returns nothing. + */ + initialize: function () { + $.proxyAll(this, /_on/); + this.button = $('#followee-filter .btn'). + on('click', this._onShowFolloweeDropdown); + var title = this.button.prop('title'); + this.button.popover({ + placement: 'bottom', + title: 'Filter', + html: true, + content: $('#followee-popover').html() + }); + this.button.prop('title', title); + this.popover = this.button.data('popover').tip().addClass('popover-followee'); + // Are there new items in the dashboard? If so... reset the + // notifications number to zero + if ($('.new', this.el)) { + setTimeout(function() { + $('.masthead .notifications').removeClass('notifications-important').html('0'); + }, 2000); + } + }, + + /* Handles click event on the 'show me:' dropdown button + * + * Returns nothing. + */ + _onShowFolloweeDropdown: function() { + this.button.toggleClass('active'); + if (this.button.hasClass('active')) { + setTimeout(this._onInitSearch, 100); + } + return false; + }, + + /* Handles focusing on the input and making sure that the keyup + * even is applied to the input + * + * Returns nothing. + */ + _onInitSearch: function() { + var input = $('input', this.popover); + if (!input.hasClass('inited')) { + input. + on('keyup', this._onSearchKeyUp). + addClass('inited'); + } + input.focus(); + }, + + /* Handles the keyup event + * + * Returns nothing. + */ + _onSearchKeyUp: function() { + clearTimeout(this.searchTimeout); + this.searchTimeout = setTimeout(this._onSearchKeyUpTimeout, 300); + }, + + /* Handles the actual filtering of search results + * + * Returns nothing. + */ + _onSearchKeyUpTimeout: function() { + var input = $('input', this.popover); + var q = input.val().toLowerCase(); + if (q) { + $('li', this.popover).hide(); + $('li.everything, [data-search^="' + q + '"]', this.popover).show(); + } else { + $('li', this.popover).show(); + } + } + }; }); diff --git a/ckan/public/base/javascript/modules/popover-context.js b/ckan/public/base/javascript/modules/popover-context.js index 31651eef06e..18a118709af 100644 --- a/ckan/public/base/javascript/modules/popover-context.js +++ b/ckan/public/base/javascript/modules/popover-context.js @@ -1,7 +1,7 @@ /* Popover context * These appear when someone hovers over a context item in a activity stream to - * give the user more context into that particular item. It also allows for people to - * follow and unfollow quickly from within the popover + * give the user more context into that particular item. It also allows for + * people to follow and unfollow quickly from within the popover * * id - The user_id of user * context - The type of this popover: currently supports user & package @@ -100,7 +100,8 @@ this.ckan.module('popover-context', function($, _) { } }, - /* Handles the showing of the popover on hover (also hides other active popovers) + /* Handles the showing of the popover on hover (also hides other active + * popovers) * * Returns nothing. */ @@ -162,7 +163,8 @@ this.ckan.module('popover-context', function($, _) { } }, - /* Used to break down a raw object into something a little more passable into a GET request + /* Used to break down a raw object into something a little more + * passable into a GET request * * Returns object. */ @@ -183,14 +185,14 @@ this.ckan.module('popover-context', function($, _) { params.title = raw.title; params.name = raw.name; params.notes = raw.notes; - params.num_resources = raw.resources.length; - params.num_tags = raw.tags.length; + params.num_resources = raw.num_resources; + params.num_tags = raw.num_tags; } else if (type == 'group') { params.id = raw.id; params.title = raw.title; params.name = raw.name; params.description = raw.description; - params.num_datasets = raw.packages.length; + params.package_count = raw.package_count; params.num_followers = raw.num_followers; } return params; @@ -244,8 +246,8 @@ this.ckan.module('popover-context', function($, _) { return false; }, - /* Callback from when you follow/unfollow a specified item... this is used to ensure - * all popovers associated to that user get re-populated + /* Callback from when you follow/unfollow a specified item... this is + * used to ensure all popovers associated to that user get re-populated * * Returns nothing. */ diff --git a/ckan/public/base/less/dashboard.less b/ckan/public/base/less/dashboard.less index 63223bbcd7b..34c524ffadb 100644 --- a/ckan/public/base/less/dashboard.less +++ b/ckan/public/base/less/dashboard.less @@ -1,5 +1,140 @@ -.module-my-datasets { +.dashboard { + .module-content { + .clearfix(); + background: transparent url("@{imagePath}/full-width-nav-right.png") 100% 0 repeat-y; + } + .dashboard-main { + float: left; + width: 630px; + margin-right: 20px; + } + .dashboard-aside { + float: left; + width: 220px; + padding-left: 20px; + .nav-list { + margin: 10px -25px 0 -20px; + li a { + padding: 5px 20px; + } + } + } +} + +#followee-filter { + .btn { + .clearfix(); + span, strong { + display: block; + float: left; + line-height: 1.5; + } + span { + font-weight: normal; + } + strong { + margin: 0 5px; + white-space: nowrap; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +.dashboard-aside-context { + position: relative; + margin: -20px -25px 20px -20px; + padding: 20px; + border-left: 1px solid #DCDCDC; + border-bottom: 1px solid #DCDCDC; + background-color: @moduleHeadingBackgroundColorStart; + .border-radius(0 5px 0 0); + h2 { + margin-bottom: 10px; + } + .arrow { + position: absolute; + content: ' '; + top: 24px; + left: -10px; + width: 10px; + height: 21px; + background: transparent url("@{imagePath}/dashboard-followee-related.png"); + } +} + +.popover-followee { + .popover-title { + display: none; + } + .popover-content { + padding: 0; + .border-radius(3px); + } .empty { padding: 10px; } + .popover-header { + .clearfix(); + background-color: whiteSmoke; + padding: 5px; + border-bottom: 1px solid @inputBorder; + .border-radius(3px 3px 0 0); + .add-on, + input { + float: left; + margin: 0; + } + .add-on { + padding: 4px 8px 4px 12px; + border-right-width: 0; + .border-radius(100px 0 0 100px); + } + input { + padding: 4px 12px 4px 8px; + font-size: 13px; + width: 211px; + .border-radius(0 100px 100px 0); + } + } + .nav { + padding: 0; + max-height: 205px; + overflow: auto; + .border-radius(0 0 3px 3px); + li { + a { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + padding: 7px 10px 7px 15px; + margin: 0; + .border-radius(0); + i { + background-color: @layoutLinkColor; + color: @inputBackground; + margin-right: 11px; + padding: 3px 5px; + line-height: 1; + .border-radius(100px); + .box-shadow(inset 0 1px 2x rgba(0, 0, 0, 0.2)); + } + &:hover i { + background-color: #000; + } + } + &.active a i { + color: @layoutLinkColor; + background-color: @inputBackground; + } + } + } +} + +.ie8 { + .dashboard-aside { + width: 218px; + } } diff --git a/ckan/public/base/less/masthead.less b/ckan/public/base/less/masthead.less index 31397c72e2d..a4e6eadd27e 100644 --- a/ckan/public/base/less/masthead.less +++ b/ckan/public/base/less/masthead.less @@ -184,6 +184,7 @@ header.masthead { } .user-dropdown-menu { + min-width: 120px; a { color: @mastheadBackgroundColorStart; &:hover { diff --git a/ckan/templates/activity_streams/activity_stream_items.html b/ckan/templates/activity_streams/activity_stream_items.html index 61a557af941..ff863425a5a 100644 --- a/ckan/templates/activity_streams/activity_stream_items.html +++ b/ckan/templates/activity_streams/activity_stream_items.html @@ -1,16 +1,20 @@ {% set has_more_length = g.activity_list_limit|int %} {% set has_more = activities|length > has_more_length %} - +{% if activities %} + +{% else %} +

{{ _('No activities are within this activity stream') }}

+{% endif %} diff --git a/ckan/templates/ajax_snippets/popover_context_group.html b/ckan/templates/ajax_snippets/popover_context_group.html index 75a1b10f83c..62255d12d10 100644 --- a/ckan/templates/ajax_snippets/popover_context_group.html +++ b/ckan/templates/ajax_snippets/popover_context_group.html @@ -19,7 +19,7 @@
{{ _('Datasets') }}
-
{{ h.SI_number_span(num_datasets) }}
+
{{ h.SI_number_span(package_count) }}
diff --git a/ckan/templates/snippets/popover_context.html b/ckan/templates/snippets/popover_context.html new file mode 100644 index 00000000000..d445d5b6162 --- /dev/null +++ b/ckan/templates/snippets/popover_context.html @@ -0,0 +1,10 @@ +{%if type == 'user' %} +

{{ dict.name }}

+ {% snippet 'ajax_snippets/popover_context_user.html', id=dict.id, name=dict.name, about=dict.about, is_me='false', num_followers=dict.num_followers, number_administered_packages=dict.number_administered_packages, number_of_edits=dict.number_of_edits %} +{%elif type == 'dataset' %} +

{{ dict.title }}

+ {% snippet 'ajax_snippets/popover_context_dataset.html', id=dict.id, name=dict.name, notes=dict.notes, num_resources=dict.num_resources, num_tags=dict.num_tags %} +{%elif type == 'group' %} +

{{ dict.title }}

+ {% snippet 'ajax_snippets/popover_context_group.html', id=dict.id, name=dict.name, description=dict.description, num_followers=dict.num_followers, package_count=dict.package_count %} +{% endif %} diff --git a/ckan/templates/user/dashboard.html b/ckan/templates/user/dashboard.html index 2beaaaddea9..40b2a9387f1 100644 --- a/ckan/templates/user/dashboard.html +++ b/ckan/templates/user/dashboard.html @@ -5,40 +5,49 @@ {% block subtitle %}{{ _('Dashboard') }}{% endblock %} {% block breadcrumb_content %} -
  • {{ _('Dashboard') }}
  • +
  • {{ _('Dashboard') }}
  • {% endblock %} {% block actions_content %} {% if h.check_access('package_create') %} -
  • {% link_for _('Add Dataset'), controller='package', action='new', class_="btn btn-primary icon-large", icon="plus" %}
  • +
  • {% link_for _('Add a dataset'), controller='package', action='new', class_="btn btn-primary icon-large", icon="plus" %}
  • {% endif %} {% endblock %} -{% block primary_content %} -
    -
    -

    {{ _('News feed') }} {{ _('Activity from users and datasets that you follow') }}

    - {{ c.dashboard_activity_stream }} +{% block primary %} +
    +
    +
    + {% snippet 'user/snippets/followee_dropdown.html', context=c.dashboard_activity_stream_context, followees=c.followee_list %} +

    + {{ _('News feed') }} + {{ _('Activity from items that you follow') }} +

    + {{ c.dashboard_activity_stream }} +
    +
    {% endblock %} -{% block secondary_content %} -
    -

    {{ _('My Datasets') }}

    - {% if c.user_dict['datasets'] %} - - {% else %} -

    {{ _('You currently have not added any datasets yet') }}

    - {% endif %} -
    -{% endblock %} +{% block sidebar %}{% endblock %} diff --git a/ckan/templates/user/snippets/followee_dropdown.html b/ckan/templates/user/snippets/followee_dropdown.html new file mode 100644 index 00000000000..c3daffbce64 --- /dev/null +++ b/ckan/templates/user/snippets/followee_dropdown.html @@ -0,0 +1,45 @@ +{% macro followee_icon(type) -%} + {% if type == 'dataset' %} + + {% elif type == 'user' %} + + {% elif type == 'group' %} + + {% endif %} +{%- endmacro %} + +
    + + {{ _('Activity from:') }} + {{ context.context }} + + + +
    diff --git a/ckan/tests/functional/api/test_activity.py b/ckan/tests/functional/api/test_activity.py index 51bee6de429..e5473df8d4c 100644 --- a/ckan/tests/functional/api/test_activity.py +++ b/ckan/tests/functional/api/test_activity.py @@ -17,7 +17,6 @@ from ckan.lib.helpers import json -##<<<<<<< HEAD ##def package_update(context, data_dict): ## # These tests call package_update directly which is really bad ## # setting api_version in context make things seem like the api key @@ -33,7 +32,6 @@ ## context['api_version'] = 3 ## context['ignore_auth'] = True ## return _package_create(context, data_dict) -##======= def package_show(app, data_dict, apikey=None): if apikey: extra_environ = {'Authorization': str(apikey)} @@ -2085,10 +2083,43 @@ def test_follow_user(self): #if 'id' not in activity: # assert False, "activity object should have an id value" # TODO: Test for the _correct_ revision_id value. + #if 'revision_id' not in activity: # assert False, "activity object should have a revision_id value" #timestamp = datetime_from_string(activity['timestamp']) #assert timestamp >= before['time'] and timestamp <= \ # after['time'], str(activity['timestamp']) - # + #assert len(self.activity_details(activity)) == 0 + + def test_user_activity_list_by_name(self): + '''user_activity_list should accept a user name as param.''' + import ckan.tests + activities = ckan.tests.call_action_api(self.app, 'user_activity_list', + id='annafan') + assert len(activities) > 0 + + def test_package_activity_list_by_name(self): + '''package_activity_list should accept a package name as param.''' + import ckan.tests + activities = ckan.tests.call_action_api(self.app, + 'package_activity_list', id='warandpeace', + apikey=self.sysadmin_user['apikey']) + assert len(activities) > 0 + + def test_group_activity_list_by_name(self): + '''group_activity_list should accept a group name as param.''' + import ckan.tests + activities = ckan.tests.call_action_api(self.app, + 'group_activity_list', id='roger') + assert len(activities) > 0 + + def test_organization_activity_list_by_name(self): + '''organization_activity_list should accept a org name as param.''' + import ckan.tests + organization = ckan.tests.call_action_api(self.app, + 'organization_create', name='test_org', + apikey=self.sysadmin_user['apikey']) + activities = ckan.tests.call_action_api(self.app, + 'organization_activity_list', id=organization['name']) + assert len(activities) > 0 diff --git a/ckan/tests/functional/api/test_follow.py b/ckan/tests/functional/api/test_follow.py index eb4530e3cea..79e6f5dab17 100644 --- a/ckan/tests/functional/api/test_follow.py +++ b/ckan/tests/functional/api/test_follow.py @@ -25,6 +25,43 @@ def datetime_from_string(s): ''' return datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f') +def follow(func): + '''Return a wrapper function for a follow_* function. + + The wrapper functions test the `followee_list` and `followee_count` API + calls, in addition to any tests carried out by the wrapped function. + + ''' + def wrapped_func(app, follower_id, apikey, object_id, object_arg, + sysadmin_apikey): + followee_count_before = ckan.tests.call_action_api(app, + 'followee_count', id=follower_id) + followees_before = ckan.tests.call_action_api(app, 'followee_list', + id=follower_id, apikey=sysadmin_apikey) + + func(app, follower_id, apikey, object_id, object_arg, sysadmin_apikey) + + followee_count_after = ckan.tests.call_action_api(app, + 'followee_count', id=follower_id) + followees_after = ckan.tests.call_action_api(app, 'followee_list', + id=follower_id, apikey=sysadmin_apikey) + + assert followee_count_after == followee_count_before + 1, ( + "After a user follows an object, the user's `followee_count` " + "should increase by 1") + + assert len(followees_after) == len(followees_before) + 1, ( + "After a user follows an object, the object should appear in " + "the user's `followee_list`") + assert len([followee for followee in followees_after + if followee['dict']['id'] == object_id]) == 1, ( + "After a user follows an object, the object should appear in " + "the user's `followee_list`") + + return wrapped_func + + +@follow def follow_user(app, follower_id, apikey, object_id, object_arg, sysadmin_apikey): '''Test a user starting to follow another user via the API. @@ -86,6 +123,8 @@ def follow_user(app, follower_id, apikey, object_id, object_arg, 'user_followee_count', id=follower_id) assert followee_count_after == followee_count_before + 1 + +@follow def follow_dataset(app, follower_id, apikey, dataset_id, dataset_arg, sysadmin_apikey): '''Test a user starting to follow a dataset via the API. @@ -147,6 +186,8 @@ def follow_dataset(app, follower_id, apikey, dataset_id, dataset_arg, 'dataset_followee_count', id=follower_id) assert followee_count_after == followee_count_before + 1 + +@follow def follow_group(app, user_id, apikey, group_id, group_arg, sysadmin_apikey): '''Test a user starting to follow a group via the API. @@ -297,6 +338,20 @@ def test_00_sysadmin_can_get_group_follower_list(self): ckan.tests.call_action_api(self.app, 'group_follower_list', id='roger', status=200, apikey=self.testsysadmin['apikey']) + def test_00_visitor_cannot_get_followee_list(self): + ckan.tests.call_action_api(self.app, 'followee_list', + id=self.russianfan['id'], status=403) + + def test_00_user_cannot_get_followee_list(self): + ckan.tests.call_action_api(self.app, 'followee_list', + id=self.russianfan['id'], status=403, + apikey=self.annafan['apikey']) + + def test_00_sysadmin_can_get_followee_list(self): + ckan.tests.call_action_api(self.app, 'followee_list', + id=self.russianfan['id'], status=200, + apikey=self.testsysadmin['apikey']) + def test_00_visitor_cannot_get_user_followee_list(self): '''A visitor cannot see what users a user is following.''' ckan.tests.call_action_api(self.app, 'user_followee_list', @@ -509,6 +564,57 @@ def test_04_group_follower_count_no_followers(self): 'group_follower_count', id=self.davids_group['id']) assert follower_count == 0 + def _followee_count_bad_id(self, action): + for object_id in ('bad id', ' ', 3, 35.7, 'xxx', ''): + error = ckan.tests.call_action_api(self.app, action, + status=409, id=object_id) + assert 'id' in error + + def test_04_followee_count_bad_id(self): + self._followee_count_bad_id('followee_count') + + def test_04_user_followee_count_bad_id(self): + self._followee_count_bad_id('user_followee_count') + + def test_04_dataset_followee_count_bad_id(self): + self._followee_count_bad_id('dataset_followee_count') + + def test_04_group_followee_count_bad_id(self): + self._followee_count_bad_id('group_followee_count') + + def _followee_count_missing_id(self, action): + error = ckan.tests.call_action_api(self.app, action, status=409) + assert error['id'] == ['Missing value'] + + def test_04_followee_count_missing_id(self): + self._followee_count_missing_id('followee_count') + + def test_04_user_followee_count_missing_id(self): + self._followee_count_missing_id('user_followee_count') + + def test_04_dataset_followee_count_missing_id(self): + self._followee_count_missing_id('dataset_followee_count') + + def test_04_group_followee_count_missing_id(self): + self._followee_count_missing_id('group_followee_count') + + def _followee_count_not_following_anything(self, action): + followee_count = ckan.tests.call_action_api(self.app, action, + id=self.russianfan['id']) + assert followee_count == 0 + + def test_04_followee_count_not_following_anything(self): + self._followee_count_not_following_anything('followee_count') + + def test_04_user_followee_count_not_following_anything(self): + self._followee_count_not_following_anything('user_followee_count') + + def test_04_dataset_followee_count_not_following_anything(self): + self._followee_count_not_following_anything('dataset_followee_count') + + def test_04_group_followee_count_not_following_anything(self): + self._followee_count_not_following_anything('group_followee_count') + def test_04_follower_list_bad_id(self): for action in ('user_follower_list', 'dataset_follower_list', 'group_follower_list'): @@ -541,6 +647,59 @@ def test_04_group_follower_list_no_followers(self): id=self.davids_group['id'], apikey=self.testsysadmin['apikey']) assert followers == [] + def _followee_list_bad_id(self, action): + for object_id in ('bad id', ' ', 3, 35.7, 'xxx', ''): + error = ckan.tests.call_action_api(self.app, action, + status=409, id=object_id, + apikey=self.testsysadmin['apikey']) + assert error['id'] + + def test_04_followee_list_bad_id(self): + self._followee_list_bad_id('followee_list') + + def test_04_user_followee_list_bad_id(self): + self._followee_list_bad_id('user_followee_list') + + def test_04_dataset_followee_list_bad_id(self): + self._followee_list_bad_id('dataset_followee_list') + + def test_04_group_followee_list_bad_id(self): + self._followee_list_bad_id('group_followee_list') + + def _followee_list_missing_id(self, action): + error = ckan.tests.call_action_api(self.app, action, status=409, + apikey=self.testsysadmin['apikey']) + assert error['id'] == ['Missing value'] + + def test_04_followee_list_missing_id(self): + self._followee_list_missing_id('followee_list') + + def test_04_user_followee_list_missing_id(self): + self._followee_list_missing_id('user_followee_list') + + def test_04_dataset_followee_missing_bad_id(self): + self._followee_list_missing_id('dataset_followee_list') + + def test_04_group_followee_missing_bad_id(self): + self._followee_list_missing_id('group_followee_list') + + def _followee_list_not_following_anything(self, action): + followees = ckan.tests.call_action_api(self.app, action, + id=self.russianfan['id'], apikey=self.russianfan['apikey']) + assert followees == [] + + def test_04_followee_list_not_following_anything(self): + self._followee_list_not_following_anything('followee_list') + + def test_04_user_followee_list_not_following_anything(self): + self._followee_list_not_following_anything('user_followee_list') + + def test_04_dataset_followee_not_following_anything(self): + self._followee_list_not_following_anything('dataset_followee_list') + + def test_04_group_followee_not_following_anything(self): + self._followee_list_not_following_anything('group_followee_list') + def test_04_am_following_bad_id(self): for action in ('am_following_dataset', 'am_following_user', 'am_following_group'): @@ -761,6 +920,10 @@ def _unfollow_user(self, follower_id, apikey, object_id, object_arg): # Record the user's number of followers before. count_before = ckan.tests.call_action_api(self.app, 'user_follower_count', id=object_id) + followee_count_before = ckan.tests.call_action_api(self.app, + 'followee_count', id=follower_id) + user_followee_count_before = ckan.tests.call_action_api(self.app, + 'user_followee_count', id=follower_id) # Check that the user is following the object. am_following = ckan.tests.call_action_api(self.app, @@ -787,6 +950,25 @@ def _unfollow_user(self, follower_id, apikey, object_id, object_arg): 'user_follower_count', id=object_id) assert count_after == count_before - 1 + # Check that the user doesn't appear in the subject's list of + # followees. + followees = ckan.tests.call_action_api(self.app, 'followee_list', + id=follower_id, apikey=apikey) + assert len([followee for followee in followees + if followee['dict']['id'] == object_id]) == 0 + followees = ckan.tests.call_action_api(self.app, 'user_followee_list', + id=follower_id, apikey=apikey) + assert len([followee for followee in followees + if followee['id'] == object_id]) == 0 + + # Check the the subject's followee cont has decreased by 1. + count_after = ckan.tests.call_action_api(self.app, 'followee_count', + id=follower_id) + assert count_after == followee_count_before - 1 + count_after = ckan.tests.call_action_api(self.app, + 'user_followee_count', id=follower_id) + assert count_after == user_followee_count_before - 1 + def _unfollow_dataset(self, user_id, apikey, dataset_id, dataset_arg): '''Test a user unfollowing a dataset via the API. @@ -800,6 +982,10 @@ def _unfollow_dataset(self, user_id, apikey, dataset_id, dataset_arg): # Record the dataset's number of followers before. count_before = ckan.tests.call_action_api(self.app, 'dataset_follower_count', id=dataset_id) + followee_count_before = ckan.tests.call_action_api(self.app, + 'followee_count', id=user_id) + dataset_followee_count_before = ckan.tests.call_action_api(self.app, + 'dataset_followee_count', id=user_id) # Check that the user is following the dataset. am_following = ckan.tests.call_action_api(self.app, @@ -828,6 +1014,25 @@ def _unfollow_dataset(self, user_id, apikey, dataset_id, dataset_arg): 'dataset_follower_count', id=dataset_id) assert count_after == count_before - 1 + # Check that the dataset doesn't appear in the user's list of + # followees. + followees = ckan.tests.call_action_api(self.app, 'followee_list', + id=user_id, apikey=apikey) + assert len([followee for followee in followees + if followee['dict']['id'] == dataset_id]) == 0 + followees = ckan.tests.call_action_api(self.app, + 'dataset_followee_list', id=user_id, apikey=apikey) + assert len([followee for followee in followees + if followee['id'] == dataset_id]) == 0 + + # Check the the user's followee count has decreased by 1. + count_after = ckan.tests.call_action_api(self.app, 'followee_count', + id=user_id) + assert count_after == followee_count_before - 1 + count_after = ckan.tests.call_action_api(self.app, + 'dataset_followee_count', id=user_id) + assert count_after == dataset_followee_count_before - 1 + def _unfollow_group(self, user_id, apikey, group_id, group_arg): '''Test a user unfollowing a group via the API. @@ -841,6 +1046,10 @@ def _unfollow_group(self, user_id, apikey, group_id, group_arg): # Record the group's number of followers before. count_before = ckan.tests.call_action_api(self.app, 'group_follower_count', id=group_id) + followee_count_before = ckan.tests.call_action_api(self.app, + 'followee_count', id=user_id) + group_followee_count_before = ckan.tests.call_action_api(self.app, + 'group_followee_count', id=user_id) # Check that the user is following the group. am_following = ckan.tests.call_action_api(self.app, @@ -868,6 +1077,26 @@ def _unfollow_group(self, user_id, apikey, group_id, group_arg): 'group_follower_count', id=group_id) assert count_after == count_before - 1 + # Check that the group doesn't appear in the user's list of + # followees. + followees = ckan.tests.call_action_api(self.app, 'followee_list', + id=user_id, apikey=apikey) + assert len([followee for followee in followees + if followee['dict']['id'] == group_id]) == 0 + followees = ckan.tests.call_action_api(self.app, + 'group_followee_list', id=user_id, + apikey=self.testsysadmin['apikey']) + assert len([followee for followee in followees + if followee['id'] == group_id]) == 0 + + # Check the the user's followee count has decreased by 1. + count_after = ckan.tests.call_action_api(self.app, 'followee_count', + id=user_id) + assert count_after == followee_count_before - 1 + count_after = ckan.tests.call_action_api(self.app, + 'group_followee_count', id=user_id) + assert count_after == group_followee_count_before - 1 + def test_02_follower_delete_by_id(self): self._unfollow_user(self.annafan['id'], self.annafan['apikey'], self.joeadmin['id'], self.joeadmin['id']) @@ -984,6 +1213,13 @@ def test_01_on_delete_cascade_api(self): status=409, id='joeadmin', apikey=self.testsysadmin['apikey']) assert 'id' in error + # It should no longer be possible to get joeadmin's followee lists. + for action in ('followee_list', 'user_followee_list', + 'dataset_followee_list', 'group_followee_list'): + error = ckan.tests.call_action_api(self.app, action, status=409, + id='joeadmin', apikey=self.testsysadmin['apikey']) + assert 'id' in error + # It should no longer be possible to get warandpeace's follower list. error = ckan.tests.call_action_api(self.app, 'dataset_follower_list', status=409, id='warandpeace', apikey=self.testsysadmin['apikey']) @@ -999,6 +1235,13 @@ def test_01_on_delete_cascade_api(self): status=409, id='joeadmin') assert 'id' in error + # It should no longer be possible to get joeadmin's followee counts. + for action in ('followee_count', 'user_followee_count', + 'dataset_followee_count', 'group_followee_count'): + error = ckan.tests.call_action_api(self.app, action, status=409, + id='joeadmin') + assert 'id' in error + # It should no longer be possible to get warandpeace's follower count. error = ckan.tests.call_action_api(self.app, 'dataset_follower_count', status=409, id='warandpeace') diff --git a/ckan/tests/lib/test_dictization.py b/ckan/tests/lib/test_dictization.py index 61828f0a27d..f868948a59b 100644 --- a/ckan/tests/lib/test_dictization.py +++ b/ckan/tests/lib/test_dictization.py @@ -124,7 +124,10 @@ def setup_class(cls): 'title': u'A Novel By Tolstoy', 'tracking_summary': {'total': 0, 'recent': 0}, 'url': u'http://www.annakarenina.com', - 'version': u'0.7a'} + 'version': u'0.7a', + 'num_tags': 3, + 'num_resources': 2, + } @classmethod @@ -256,6 +259,8 @@ def test_03_package_to_api1(self): asdict = pkg.as_dict() asdict['download_url'] = asdict['resources'][0]['url'] asdict['license_title'] = u'Other (Open)' + asdict['num_tags'] = 3 + asdict['num_resources'] = 2 dictize = package_to_api1(pkg, context) # the is_dict method doesn't care about organizations @@ -274,6 +279,8 @@ def test_04_package_to_api1_with_relationship(self): as_dict = pkg.as_dict() as_dict['license_title'] = None + as_dict['num_tags'] = 0 + as_dict['num_resources'] = 0 dictize = package_to_api1(pkg, context) as_dict["relationships"].sort(key=lambda x:x.items()) @@ -314,6 +321,8 @@ def test_06_package_to_api2_with_relationship(self): as_dict = pkg.as_dict(ref_package_by='id', ref_group_by='id') as_dict['license_title'] = None + as_dict['num_tags'] = 0 + as_dict['num_resources'] = 0 dictize = package_to_api2(pkg, context) as_dict["relationships"].sort(key=lambda x:x.items()) @@ -739,12 +748,15 @@ def test_13_get_package_in_past(self): u'url': u'http://newurl', u'webstore_last_updated': None, u'webstore_url': None}) + third_dictized['num_resources'] = third_dictized['num_resources'] + 1 third_dictized['tags'].insert(1, {'name': u'newnew_tag', 'display_name': u'newnew_tag', 'state': 'active'}) + third_dictized['num_tags'] = third_dictized['num_tags'] + 1 third_dictized['extras'].insert(0, {'key': 'david', 'value': u'"new_value"', 'state': u'active'}) third_dictized['state'] = 'active' + third_dictized['state'] = 'active' pprint(third_dictized) pprint(forth_dictized)