From 4145f00a4a046ad321183140d2c75fac0b55c651 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 23 Oct 2012 19:22:13 +0200 Subject: [PATCH 01/14] [#3005] Add model and API for following groups This implements everything required at the model and API level for following and unfollowing of groups, getting a group's number of followers or a list of a group's followers, and getting the number of groups that a user follows or a list of the groups that a user follows. Tests need to be added. Frontend for following groups needs to be added. + Add UserFollowingGroup model class and user_following_group_dictize(). + Add "{actor} started following {group}" activity stream activity. + Refactor user_following_user_dict_save() and user_following_dataset_dict_save(), replace with a single follower_dict_save() that can be used for UserFollowingUser, UserFollowingDataset, or UserFollowingGroup. + Add follow_group() and unfollow_group() logic action functions in create.py and delete.py. + Add group_follower_count(), group_follower_list(), am_following_group(), group_followee_count() and group_followee_list() logic action functions in get.py. Also refactor some code in logic/action/get.py to remove duplication between follower functions. + Add convert_group_name_or_id_to_id() converter function + Add default_follow_group_schema() + Add migration script --- ckan/lib/activity_streams.py | 5 + ckan/lib/dictization/model_dictize.py | 3 + ckan/lib/dictization/model_save.py | 13 +- ckan/logic/action/create.py | 79 +++++++- ckan/logic/action/delete.py | 19 +- ckan/logic/action/get.py | 176 +++++++++++++----- ckan/logic/converters.py | 25 +++ ckan/logic/schema.py | 8 +- ckan/logic/validators.py | 1 + .../versions/061_add_follower__group_table.py | 22 +++ ckan/model/__init__.py | 1 + ckan/model/follower.py | 71 +++++++ 12 files changed, 360 insertions(+), 63 deletions(-) create mode 100644 ckan/migration/versions/061_add_follower__group_table.py diff --git a/ckan/lib/activity_streams.py b/ckan/lib/activity_streams.py index e047c9c92f5..8d1d6fea515 100644 --- a/ckan/lib/activity_streams.py +++ b/ckan/lib/activity_streams.py @@ -110,6 +110,9 @@ def activity_stream_string_follow_dataset(): def activity_stream_string_follow_user(): return _("{actor} started following {user}") +def activity_stream_string_follow_group(): + return _("{actor} started following {group}") + def activity_stream_string_new_related_item(): return _("{actor} created the link to related {related_type} {related_item}") @@ -148,6 +151,7 @@ def activity_stream_string_new_related_item(): 'deleted related item': activity_stream_string_deleted_related_item, 'follow dataset': activity_stream_string_follow_dataset, 'follow user': activity_stream_string_follow_user, + 'follow group': activity_stream_string_follow_group, 'new related item': activity_stream_string_new_related_item, } @@ -172,6 +176,7 @@ def activity_stream_string_new_related_item(): 'deleted related item': 'picture', 'follow dataset': 'sitemap', 'follow user': 'user', + 'follow group': 'groups', 'new related item': 'picture', } diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index 97869588391..8a08ff50b03 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -575,3 +575,6 @@ def user_following_user_dictize(follower, context): def user_following_dataset_dictize(follower, context): return d.table_dictize(follower, context) + +def user_following_group_dictize(follower, context): + return d.table_dictize(follower, context) diff --git a/ckan/lib/dictization/model_save.py b/ckan/lib/dictization/model_save.py index d19ad918dee..41f901b4f0e 100644 --- a/ckan/lib/dictization/model_save.py +++ b/ckan/lib/dictization/model_save.py @@ -594,19 +594,10 @@ def tag_dict_save(tag_dict, context): tag = d.table_dict_save(tag_dict, model.Tag, context) return tag -def user_following_user_dict_save(data_dict, context): +def follower_dict_save(data_dict, context, FollowerClass): model = context['model'] session = context['session'] - follower_obj = model.UserFollowingUser( - follower_id=model.User.get(context['user']).id, - object_id=data_dict['id']) - session.add(follower_obj) - return follower_obj - -def user_following_dataset_dict_save(data_dict, context): - model = context['model'] - session = context['session'] - follower_obj = model.UserFollowingDataset( + follower_obj = FollowerClass( follower_id=model.User.get(context['user']).id, object_id=data_dict['id']) session.add(follower_obj) diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index dc25af1a8b5..2fece287b10 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -924,8 +924,8 @@ def follow_user(context, data_dict): 'You are already following {0}').format(data_dict['id']) raise ValidationError({'message': message}, error_summary=message) - follower = model_save.user_following_user_dict_save(validated_data_dict, - context) + follower = model_save.follower_dict_save(validated_data_dict, context, + model.UserFollowingUser) activity_dict = { 'user_id': userobj.id, @@ -995,8 +995,8 @@ def follow_dataset(context, data_dict): 'You are already following {0}').format(data_dict['id']) raise ValidationError({'message': message}, error_summary=message) - follower = model_save.user_following_dataset_dict_save( - validated_data_dict, context) + follower = model_save.follower_dict_save(validated_data_dict, context, + model.UserFollowingDataset) activity_dict = { 'user_id': userobj.id, @@ -1023,3 +1023,74 @@ def follow_dataset(context, data_dict): follower=follower.follower_id, object=follower.object_id)) return model_dictize.user_following_dataset_dictize(follower, context) + + +def follow_group(context, data_dict): + '''Start following a group. + + You must provide your API key in the Authorization header. + + :param id: the id or name of the group to follow, e.g. ``'roger'`` + :type id: string + + :returns: a representation of the 'follower' relationship between yourself + and the group + :rtype: dictionary + + ''' + if 'user' not in context: + raise logic.NotAuthorized( + _("You must be logged in to follow a group.")) + + model = context['model'] + session = context['session'] + + userobj = model.User.get(context['user']) + if not userobj: + raise logic.NotAuthorized( + _("You must be logged in to follow a group.")) + + schema = context.get('schema', + ckan.logic.schema.default_follow_group_schema()) + + validated_data_dict, errors = _validate(data_dict, schema, context) + + if errors: + model.Session.rollback() + raise ValidationError(errors) + + # Don't let a user follow a group she is already following. + if model.UserFollowingGroup.is_following(userobj.id, + validated_data_dict['id']): + message = _( + 'You are already following {0}').format(data_dict['id']) + raise ValidationError({'message': message}, error_summary=message) + + follower = model_save.follower_dict_save(validated_data_dict, context, + model.UserFollowingGroup) + + activity_dict = { + 'user_id': userobj.id, + 'object_id': validated_data_dict['id'], + 'activity_type': 'follow group', + } + activity_dict['data'] = { + 'group': ckan.lib.dictization.table_dictize( + model.Group.get(validated_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() + + log.debug(u'User {follower} started following group {object}'.format( + follower=follower.follower_id, object=follower.object_id)) + + return model_dictize.user_following_group_dictize(follower, context) diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index 08539220444..22246be7f5a 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -312,10 +312,6 @@ def package_relationship_delete_rest(context, data_dict): package_relationship_delete(context, data_dict) def _unfollow(context, data_dict, schema, FollowerClass): - validated_data_dict, errors = validate(data_dict, schema, context) - if errors: - raise ValidationError(errors) - model = context['model'] if not context.has_key('user'): @@ -327,6 +323,9 @@ def _unfollow(context, data_dict, schema, FollowerClass): _("You must be logged in to unfollow something.")) follower_id = userobj.id + validated_data_dict, errors = validate(data_dict, schema, context) + if errors: + raise ValidationError(errors) object_id = validated_data_dict.get('id') follower_obj = FollowerClass.get(follower_id, object_id) @@ -359,3 +358,15 @@ def unfollow_dataset(context, data_dict): ckan.logic.schema.default_follow_dataset_schema()) _unfollow(context, data_dict, schema, context['model'].UserFollowingDataset) + +def unfollow_group(context, data_dict): + '''Stop following a group. + + :param id: the id or name of the group to stop following + :type id: string + + ''' + schema = context.get('schema', + ckan.logic.schema.default_follow_group_schema()) + _unfollow(context, data_dict, schema, + context['model'].UserFollowingGroup) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index afe32cc202c..499b5f56661 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1857,6 +1857,15 @@ def recently_changed_packages_activity_list_html(context, data_dict): data_dict) return activity_streams.activity_list_to_html(context, activity_stream) + +def _follower_count(context, data_dict, default_schema, ModelClass): + schema = context.get('schema', default_schema) + data_dict, errors = _validate(data_dict, schema, context) + if errors: + raise ValidationError(errors) + return ModelClass.follower_count(data_dict['id']) + + def user_follower_count(context, data_dict): '''Return the number of followers of a user. @@ -1866,12 +1875,10 @@ def user_follower_count(context, data_dict): :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) - return ckan.model.UserFollowingUser.follower_count(data_dict['id']) + return _follower_count(context, data_dict, + ckan.logic.schema.default_follow_user_schema(), + context['model'].UserFollowingUser) + def dataset_follower_count(context, data_dict): '''Return the number of followers of a dataset. @@ -1882,14 +1889,31 @@ def dataset_follower_count(context, data_dict): :rtype: int ''' - schema = context.get('schema') or ( - ckan.logic.schema.default_follow_dataset_schema()) + return _follower_count(context, data_dict, + ckan.logic.schema.default_follow_dataset_schema(), + context['model'].UserFollowingDataset) + + +def group_follower_count(context, data_dict): + '''Return the number of followers of a group. + + :param id: the id or name of the group + :type id: string + + :rtype: int + + ''' + return _follower_count(context, data_dict, + ckan.logic.schema.default_follow_group_schema(), + context['model'].UserFollowingGroup) + + +def _follower_list(context, data_dict, default_schema, FollowerClass): + schema = context.get('schema', default_schema) data_dict, errors = _validate(data_dict, schema, context) if errors: raise ValidationError(errors) - return ckan.model.UserFollowingDataset.follower_count(data_dict['id']) -def _follower_list(context, data_dict, FollowerClass): # Get the list of Follower objects. model = context['model'] object_id = data_dict.get('id') @@ -1902,6 +1926,7 @@ def _follower_list(context, data_dict, FollowerClass): # Dictize the list of User objects. return model_dictize.user_list_dictize(users, context) + def user_follower_list(context, data_dict): '''Return the list of users that are following the given user. @@ -1911,14 +1936,11 @@ def user_follower_list(context, data_dict): :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) return _follower_list(context, data_dict, + ckan.logic.schema.default_follow_user_schema(), context['model'].UserFollowingUser) + def dataset_follower_list(context, data_dict): '''Return the list of users that are following the given dataset. @@ -1928,16 +1950,32 @@ def dataset_follower_list(context, data_dict): :rtype: list of dictionaries ''' - schema = context.get('schema') or ( - ckan.logic.schema.default_follow_dataset_schema()) + return _follower_list(context, data_dict, + ckan.logic.schema.default_follow_dataset_schema(), + context['model'].UserFollowingDataset) + + +def group_follower_list(context, data_dict): + '''Return the list of users that are following the given group. + + :param id: the id or name of the group + :type id: string + + :rtype: list of dictionaries + + ''' + return _follower_list(context, data_dict, + ckan.logic.schema.default_follow_group_schema(), + context['model'].UserFollowingGroup) + + +def _am_following(context, data_dict, default_schema, FollowerClass): + schema = context.get('schema', default_schema) data_dict, errors = _validate(data_dict, schema, context) if errors: raise ValidationError(errors) - return _follower_list(context, data_dict, - context['model'].UserFollowingDataset) -def _am_following(context, data_dict, FollowerClass): - if not context.has_key('user'): + if 'user' not in context: raise logic.NotAuthorized model = context['model'] @@ -1950,6 +1988,7 @@ def _am_following(context, data_dict, FollowerClass): return FollowerClass.is_following(userobj.id, object_id) + def am_following_user(context, data_dict): '''Return ``True`` if you're following the given user, ``False`` if not. @@ -1959,15 +1998,11 @@ def am_following_user(context, data_dict): :rtype: boolean ''' - 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) - return _am_following(context, data_dict, + ckan.logic.schema.default_follow_user_schema(), context['model'].UserFollowingUser) + def am_following_dataset(context, data_dict): '''Return ``True`` if you're following the given dataset, ``False`` if not. @@ -1977,14 +2012,33 @@ def am_following_dataset(context, data_dict): :rtype: boolean ''' - schema = context.get('schema') or ( - ckan.logic.schema.default_follow_dataset_schema()) + return _am_following(context, data_dict, + ckan.logic.schema.default_follow_dataset_schema(), + context['model'].UserFollowingDataset) + + +def am_following_group(context, data_dict): + '''Return ``True`` if you're following the given group, ``False`` if not. + + :param id: the id or name of the group + :type id: string + + :rtype: boolean + + ''' + return _am_following(context, data_dict, + ckan.logic.schema.default_follow_group_schema(), + context['model'].UserFollowingGroup) + + +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) + return FollowerClass.followee_count(data_dict['id']) - 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. @@ -1995,12 +2049,9 @@ def user_followee_count(context, data_dict): :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) - return ckan.model.UserFollowingUser.followee_count(data_dict['id']) + return _followee_count(context, data_dict, + context['model'].UserFollowingUser) + def dataset_followee_count(context, data_dict): '''Return the number of datasets that are followed by the given user. @@ -2011,12 +2062,22 @@ def dataset_followee_count(context, data_dict): :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) - return ckan.model.UserFollowingDataset.followee_count(data_dict['id']) + return _followee_count(context, data_dict, + context['model'].UserFollowingDataset) + + +def group_followee_count(context, data_dict): + '''Return the number of groups that are followed by the given user. + + :param id: the id of the user + :type id: string + + :rtype: int + + ''' + return _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. @@ -2072,6 +2133,35 @@ def dataset_followee_list(context, data_dict): # Dictize the list of Package objects. return [model_dictize.package_dictize(dataset, context) for dataset in datasets] + +def group_followee_list(context, data_dict): + '''Return the list of groups 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', + 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'] + user_id = data_dict.get('id') + followees = model.UserFollowingGroup.followee_list(user_id) + + # Convert the UserFollowingGroup objects to a list of Group objects. + groups = [model.Group.get(followee.object_id) for followee in followees] + groups = [group for group in groups if group is not None] + + # Dictize the list of Group objects. + return [model_dictize.group_dictize(group, context) for group in groups] + + def dashboard_activity_list(context, data_dict): '''Return the dashboard activity stream of the given user. diff --git a/ckan/logic/converters.py b/ckan/logic/converters.py index beb7e773806..a1270a59146 100644 --- a/ckan/logic/converters.py +++ b/ckan/logic/converters.py @@ -130,3 +130,28 @@ def convert_package_name_or_id_to_id(package_name_or_id, context): if not result: raise Invalid('%s: %s' % (_('Not found'), _('Dataset'))) return result.id + +def convert_group_name_or_id_to_id(group_name_or_id, context): + '''Return the group id for the given group name or id. + + The point of this function is to convert group names to ids. If you have + something that may be a group name or id you can pass it into this + function and get the id out either way. + + Also validates that a group with the given name or id exists. + + :returns: the id of the group with the given name or id + :rtype: string + :raises: ckan.lib.navl.dictization_functions.Invalid if there is no + group with the given name or id + + ''' + session = context['session'] + result = session.query(model.Group).filter_by( + id=group_name_or_id).first() + if not result: + result = session.query(model.Group).filter_by( + name=group_name_or_id).first() + if not result: + raise Invalid('%s: %s' % (_('Not found'), _('Group'))) + return result.id diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index f8b8a946257..b1bd1a9a833 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -41,7 +41,8 @@ tag_not_in_vocabulary, url_validator) from ckan.logic.converters import (convert_user_name_or_id_to_id, - convert_package_name_or_id_to_id,) + convert_package_name_or_id_to_id, + convert_group_name_or_id_to_id,) from formencode.validators import OneOf import ckan.model @@ -428,3 +429,8 @@ def default_follow_dataset_schema(): schema = {'id': [not_missing, not_empty, unicode, convert_package_name_or_id_to_id]} return schema + +def default_follow_group_schema(): + schema = {'id': [not_missing, not_empty, unicode, + convert_group_name_or_id_to_id]} + return schema diff --git a/ckan/logic/validators.py b/ckan/logic/validators.py index 6511accc6ae..5a4b2abbaab 100644 --- a/ckan/logic/validators.py +++ b/ckan/logic/validators.py @@ -187,6 +187,7 @@ def activity_type_exists(activity_type): 'new group' : group_id_exists, 'changed group' : group_id_exists, 'deleted group' : group_id_exists, + 'follow group' : group_id_exists, 'new related item': related_id_exists, 'deleted related item': related_id_exists } diff --git a/ckan/migration/versions/061_add_follower__group_table.py b/ckan/migration/versions/061_add_follower__group_table.py new file mode 100644 index 00000000000..0fd2ad89d64 --- /dev/null +++ b/ckan/migration/versions/061_add_follower__group_table.py @@ -0,0 +1,22 @@ +from sqlalchemy import * +from migrate import * + +def upgrade(migrate_engine): + metadata = MetaData() + metadata.bind = migrate_engine + migrate_engine.execute(''' +CREATE TABLE user_following_group ( + follower_id text NOT NULL, + object_id text NOT NULL, + datetime timestamp without time zone NOT NULL +); + +ALTER TABLE user_following_group + ADD CONSTRAINT user_following_group_pkey PRIMARY KEY (follower_id, object_id); + +ALTER TABLE user_following_group + ADD CONSTRAINT user_following_group_user_id_fkey FOREIGN KEY (follower_id) REFERENCES "user"(id) ON UPDATE CASCADE ON DELETE CASCADE; + +ALTER TABLE user_following_group + ADD CONSTRAINT user_following_group_group_id_fkey FOREIGN KEY (object_id) REFERENCES "group"(id) ON UPDATE CASCADE ON DELETE CASCADE; + ''') diff --git a/ckan/model/__init__.py b/ckan/model/__init__.py index 98c647e74f4..5601f79bf5c 100644 --- a/ckan/model/__init__.py +++ b/ckan/model/__init__.py @@ -135,6 +135,7 @@ from follower import ( UserFollowingUser, UserFollowingDataset, + UserFollowingGroup, ) from system_info import ( system_info_table, diff --git a/ckan/model/follower.py b/ckan/model/follower.py index 0b3240ac9b7..6e6096ed5cc 100644 --- a/ckan/model/follower.py +++ b/ckan/model/follower.py @@ -148,3 +148,74 @@ def follower_list(cls, dataset_id): ) meta.mapper(UserFollowingDataset, user_following_dataset_table) + + +class UserFollowingGroup(domain_object.DomainObject): + '''A many-many relationship between users and groups. + + A relationship between a user (the follower) and a group (the object), + that means that the user is currently following the group. + + ''' + def __init__(self, follower_id, object_id): + self.follower_id = follower_id + self.object_id = object_id + self.datetime = datetime.datetime.now() + + @classmethod + def get(self, follower_id, object_id): + '''Return a UserFollowingGroup object for the given follower_id and + object_id, or None if no such relationship exists. + + ''' + query = meta.Session.query(UserFollowingGroup) + query = query.filter(UserFollowingGroup.follower_id == follower_id) + query = query.filter(UserFollowingGroup.object_id == object_id) + return query.first() + + @classmethod + def is_following(cls, follower_id, object_id): + '''Return True if follower_id is currently following object_id, False + otherwise. + + ''' + return UserFollowingGroup.get(follower_id, object_id) is not None + + @classmethod + def followee_count(cls, follower_id): + '''Return the number of groups followed by a user.''' + return meta.Session.query(UserFollowingGroup).filter( + UserFollowingGroup.follower_id == follower_id).count() + + @classmethod + def followee_list(cls, follower_id): + '''Return a list of groups followed by a user.''' + return meta.Session.query(UserFollowingGroup).filter( + UserFollowingGroup.follower_id == follower_id).all() + + @classmethod + def follower_count(cls, object_id): + '''Return the number of users following a group.''' + return meta.Session.query(UserFollowingGroup).filter( + UserFollowingGroup.object_id == object_id).count() + + @classmethod + def follower_list(cls, object_id): + '''Return a list of the users following a group.''' + return meta.Session.query(UserFollowingGroup).filter( + UserFollowingGroup.object_id == object_id).all() + +user_following_group_table = sqlalchemy.Table('user_following_group', + meta.metadata, + sqlalchemy.Column('follower_id', sqlalchemy.types.UnicodeText, + sqlalchemy.ForeignKey('user.id', onupdate='CASCADE', + ondelete='CASCADE'), + primary_key=True, nullable=False), + sqlalchemy.Column('object_id', sqlalchemy.types.UnicodeText, + sqlalchemy.ForeignKey('group.id', onupdate='CASCADE', + ondelete='CASCADE'), + primary_key=True, nullable=False), + sqlalchemy.Column('datetime', sqlalchemy.types.DateTime, nullable=False), +) + +meta.mapper(UserFollowingGroup, user_following_group_table) From 21429c6b7a10c31e0ea0be4c378e7c496b534d73 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 23 Oct 2012 19:51:23 +0200 Subject: [PATCH 02/14] [#3005] Add tests for follow group API Also refactored some test methods to remove code duplication between user, dataset and group follower tests. I started getting DetachedInstanceErrors from SQLAlchemy. It happens because the tests access the model directly and retain references to model objects. No idea why it started happening now but wasn't happening before. I've added an AttributeDict hack at the top of the file to remove this without introducing too much noise in the diff. Will remove the hack and fix it properly in a separate commit. --- ckan/tests/functional/api/test_follow.py | 679 ++++++++++++++--------- 1 file changed, 427 insertions(+), 252 deletions(-) diff --git a/ckan/tests/functional/api/test_follow.py b/ckan/tests/functional/api/test_follow.py index a5e8539e9ca..05ecf4c8140 100644 --- a/ckan/tests/functional/api/test_follow.py +++ b/ckan/tests/functional/api/test_follow.py @@ -191,19 +191,148 @@ def follow_dataset(app, follower_id, apikey, dataset_id, dataset_arg): assert response['success'] is True assert response['result'] == followee_count_before + 1 +def follow_group(app, user_id, apikey, group_id, group_arg): + '''Test a user starting to follow a group via the API. + + :param user_id: id of the user + :param apikey: API key of the user + :param group_id: id of the group + :param group_arg: the argument to pass to follow_group as the id of + the group that will be followed, could be the group's id or name + + ''' + # Record the group's followers count before. + params = json.dumps({'id': group_id}) + response = app.post('/api/action/group_follower_count', + params=params).json + assert response['success'] is True + follower_count_before = response['result'] + + # Record the user's followees count before. + params = json.dumps({'id': user_id}) + response = app.post('/api/action/group_followee_count', + params=params).json + assert response['success'] is True + followee_count_before = response['result'] + + # Check that the user is not already following the group. + params = json.dumps({'id': group_id}) + extra_environ = {'Authorization': str(apikey)} + response = app.post('/api/action/am_following_group', + params=params, extra_environ=extra_environ).json + assert response['success'] is True + assert response['result'] is False + + # Make the user start following the group. + before = datetime.datetime.now() + params = {'id': group_id} + extra_environ = {'Authorization': str(apikey)} + response = app.post('/api/action/follow_group', + params=json.dumps(params), extra_environ=extra_environ).json + after = datetime.datetime.now() + assert response['success'] is True + assert response['result'] + follower = response['result'] + assert follower['follower_id'] == user_id + assert follower['object_id'] == group_id + timestamp = datetime_from_string(follower['datetime']) + assert (timestamp >= before and timestamp <= after), str(timestamp) + + # Check that am_following_group now returns True. + params = json.dumps({'id': group_id}) + extra_environ = {'Authorization': str(apikey)} + response = app.post('/api/action/am_following_group', + params=params, extra_environ=extra_environ).json + assert response['success'] is True + assert response['result'] is True + + # Check that the user appears in the group's list of followers. + params = json.dumps({'id': group_id}) + response = app.post('/api/action/group_follower_list', + params=params).json + assert response['success'] is True + assert response['result'] + followers = response['result'] + assert len(followers) == follower_count_before + 1 + assert len([follower for follower in followers + if follower['id'] == user_id]) == 1 + + # Check that the group appears in the user's list of followees. + params = json.dumps({'id': user_id}) + response = app.post('/api/action/group_followee_list', + params=params).json + assert response['success'] is True + assert response['result'] + followees = response['result'] + assert len(followees) == followee_count_before + 1 + assert len([followee for followee in followees + if followee['id'] == group_id]) == 1 + + # Check that the group's follower count has increased by 1. + params = json.dumps({'id': group_id}) + response = app.post('/api/action/group_follower_count', + params=params).json + assert response['success'] is True + assert response['result'] == follower_count_before + 1 + + # Check that the user's followee count has increased by 1. + params = json.dumps({'id': user_id}) + response = app.post('/api/action/group_followee_count', + params=params).json + assert response['success'] is True + assert response['result'] == followee_count_before + 1 + + +class AttributeDict(dict): + def __getattr__(self, attr): + return self[attr] + + def __setattr__(self, attr, value): + self[attr] = value + + class TestFollow(object): '''Tests for the follower API.''' @classmethod def setup_class(self): ckan.tests.CreateTestData.create() - self.testsysadmin = ckan.model.User.get('testsysadmin') - self.annafan = ckan.model.User.get('annafan') - self.russianfan = ckan.model.User.get('russianfan') - self.tester = ckan.model.User.get('tester') - self.joeadmin = ckan.model.User.get('joeadmin') - self.warandpeace = ckan.model.Package.get('warandpeace') - self.annakarenina = ckan.model.Package.get('annakarenina') + self.testsysadmin = AttributeDict( + id = ckan.model.User.get('testsysadmin').id, + apikey = ckan.model.User.get('testsysadmin').apikey, + name = ckan.model.User.get('testsysadmin').name, + ) + self.annafan = AttributeDict({ + 'id': ckan.model.User.get('annafan').id, + 'apikey': ckan.model.User.get('annafan').apikey, + 'name': ckan.model.User.get('annafan').name, + }) + self.russianfan = AttributeDict({ + 'id': ckan.model.User.get('russianfan').id, + 'apikey': ckan.model.User.get('russianfan').apikey, + 'name': ckan.model.User.get('russianfan').name, + }) + self.joeadmin = AttributeDict({ + 'id': ckan.model.User.get('joeadmin').id, + 'apikey': ckan.model.User.get('joeadmin').apikey, + 'name': ckan.model.User.get('joeadmin').name, + }) + self.warandpeace = AttributeDict({ + 'id': ckan.model.Package.get('warandpeace').id, + 'name': ckan.model.Package.get('warandpeace').name, + }) + self.annakarenina = AttributeDict({ + 'id': ckan.model.Package.get('annakarenina').id, + 'name': ckan.model.Package.get('annakarenina').name, + }) + self.rogers_group = AttributeDict({ + 'id': ckan.model.Group.get('roger').id, + 'name': ckan.model.Group.get('roger').name, + }) + self.davids_group = AttributeDict({ + 'id': ckan.model.Group.get('david').id, + 'name': ckan.model.Group.get('david').name, + }) self.app = paste.fixture.TestApp(pylons.test.pylonsapp) @classmethod @@ -213,80 +342,81 @@ def teardown_class(self): def test_01_user_follow_user_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): params = json.dumps({'id': self.russianfan.id}) - extra_environ = { - 'Authorization': apikey, - } + extra_environ = {'Authorization': apikey} response = self.app.post('/api/action/follow_user', params=params, extra_environ=extra_environ, status=403).json - assert response['success'] == False + assert response['success'] is False assert response['error']['message'] == 'Access denied' def test_01_user_follow_dataset_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): params = json.dumps({'id': self.warandpeace.id}) - extra_environ = { - 'Authorization': 'bad api key' - } + extra_environ = {'Authorization': apikey} response = self.app.post('/api/action/follow_dataset', params=params, extra_environ=extra_environ, status=403).json - assert response['success'] == False + assert response['success'] is False + assert response['error']['message'] == 'Access denied' + + def test_01_user_follow_group_bad_apikey(self): + for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): + params = json.dumps({'id': self.rogers_group.id}) + extra_environ = {'Authorization': apikey} + response = self.app.post('/api/action/follow_group', + params=params, extra_environ=extra_environ, status=403).json + assert response['success'] is False assert response['error']['message'] == 'Access denied' def test_01_user_follow_user_missing_apikey(self): params = json.dumps({'id': self.russianfan.id}) response = self.app.post('/api/action/follow_user', params=params, status=403).json - assert response['success'] == False + assert response['success'] is False assert response['error']['message'] == 'Access denied' def test_01_user_follow_dataset_missing_apikey(self): params = json.dumps({'id': self.warandpeace.id}) response = self.app.post('/api/action/follow_dataset', params=params, status=403).json - assert response['success'] == False + assert response['success'] is False assert response['error']['message'] == 'Access denied' - def test_01_user_follow_user_bad_object_id(self): - for object_id in ('bad id', '', ' ', None, 3, 35.7, 'xxx'): - params = json.dumps({'id': 'bad id'}) - extra_environ = { - 'Authorization': str(self.annafan.apikey), - } - response = self.app.post('/api/action/follow_user', - params=params, extra_environ=extra_environ, status=409).json - assert response['success'] == False - assert response['error']['id'] == ['Not found: User'] - - def test_01_user_follow_dataset_bad_object_id(self): - for object_id in ('bad id', '', ' ', None, 3, 35.7, 'xxx'): - params = json.dumps({'id': 'bad id'}) - extra_environ = { - 'Authorization': str(self.annafan.apikey), - } - response = self.app.post('/api/action/follow_dataset', - params=params, extra_environ=extra_environ, status=409).json - assert response['success'] == False - assert response['error']['id'] == ['Not found: Dataset'] + def test_01_user_follow_group_missing_apikey(self): + params = json.dumps({'id': self.rogers_group.id}) + response = self.app.post('/api/action/follow_group', + params=params, status=403).json + assert response['success'] is False + assert response['error']['message'] == 'Access denied' - def test_01_user_follow_user_missing_object_id(self): - params = json.dumps({}) - extra_environ = { - 'Authorization': str(self.annafan.apikey), - } - response = self.app.post('/api/action/follow_user', - params=params, extra_environ=extra_environ, status=409).json - assert response['success'] == False - assert response['error']['id'] == ['Missing value'] + def test_01_follow_bad_object_id(self): + for action in ('follow_user', 'follow_dataset', 'follow_group'): + for object_id in ('bad id', ' ', 3, 35.7, 'xxx'): + params = json.dumps({'id': object_id}) + extra_environ = {'Authorization': str(self.annafan.apikey)} + response = self.app.post('/api/action/{0}'.format(action), + params=params, extra_environ=extra_environ, + status=409).json + assert response['success'] is False + assert response['error']['id'][0].startswith('Not found') + + def test_01_follow_empty_object_id(self): + for action in ('follow_user', 'follow_dataset', 'follow_group'): + for object_id in ('', None): + params = json.dumps({'id': object_id}) + extra_environ = {'Authorization': str(self.annafan.apikey)} + response = self.app.post('/api/action/{0}'.format(action), + params=params, extra_environ=extra_environ, + status=409).json + assert response['success'] is False + assert response['error']['id'] == ['Missing value'] - def test_01_user_follow_dataset_missing_object_id(self): - params = json.dumps({}) - extra_environ = { - 'Authorization': str(self.annafan.apikey), - } - response = self.app.post('/api/action/follow_dataset', - params=params, extra_environ=extra_environ, status=409).json - assert response['success'] == False - assert response['error']['id'] == ['Missing value'] + def test_01_follow_missing_object_id(self): + for action in ('follow_user', 'follow_dataset', 'follow_group'): + params = json.dumps({}) + extra_environ = {'Authorization': str(self.annafan.apikey)} + response = self.app.post('/api/action/{0}'.format(action), + params=params, extra_environ=extra_environ, status=409).json + assert response['success'] is False + assert response['error']['id'] == ['Missing value'] def test_02_user_follow_user_by_id(self): follow_user(self.app, self.annafan.id, self.annafan.apikey, @@ -296,6 +426,10 @@ def test_02_user_follow_dataset_by_id(self): follow_dataset(self.app, self.annafan.id, self.annafan.apikey, self.warandpeace.id, self.warandpeace.id) + def test_02_user_follow_group_by_id(self): + follow_group(self.app, self.annafan.id, self.annafan.apikey, + self.rogers_group.id, self.rogers_group.id) + def test_02_user_follow_user_by_name(self): follow_user(self.app, self.annafan.id, self.annafan.apikey, self.testsysadmin.id, self.testsysadmin.name) @@ -304,6 +438,10 @@ def test_02_user_follow_dataset_by_name(self): follow_dataset(self.app, self.joeadmin.id, self.joeadmin.apikey, self.warandpeace.id, self.warandpeace.name) + def test_02_user_follow_group_by_name(self): + follow_group(self.app, self.joeadmin.id, self.joeadmin.apikey, + self.rogers_group.id, self.rogers_group.name) + def test_03_user_follow_user_already_following(self): for object_id in (self.russianfan.id, self.russianfan.name, self.testsysadmin.id, self.testsysadmin.name): @@ -329,6 +467,18 @@ def test_03_user_follow_dataset_already_following(self): assert response['error']['message'].startswith( 'You are already following ') + def test_03_user_follow_group_already_following(self): + for group_id in (self.rogers_group.id, self.rogers_group.name): + params = json.dumps({'id': group_id}) + extra_environ = { + 'Authorization': str(self.annafan.apikey), + } + response = self.app.post('/api/action/follow_group', + params=params, extra_environ=extra_environ, status=409).json + assert response['success'] is False + assert response['error']['message'].startswith( + 'You are already following ') + def test_03_user_cannot_follow_herself(self): params = json.dumps({'id': self.annafan.id}) extra_environ = { @@ -336,41 +486,27 @@ def test_03_user_cannot_follow_herself(self): } response = self.app.post('/api/action/follow_user', params=params, extra_environ=extra_environ, status=409).json - assert response['success'] == False + assert response['success'] is False assert response['error']['message'] == 'You cannot follow yourself' - def test_04_user_follower_count_bad_id(self): - # user_follower_count always succeeds, but just returns 0 for bad IDs. - for object_id in ('bad id', ' ', 3, 35.7, 'xxx', ''): - params = json.dumps({'id': object_id}) - response = self.app.post('/api/action/user_follower_count', - params=params, status=409).json - assert response['success'] is False - assert response['error'].has_key('id') - - def test_04_dataset_follower_count_bad_id(self): - # dataset_follower_count always succeeds, but just returns 0 for bad - # IDs. - for object_id in ('bad id', ' ', 3, 35.7, 'xxx', ''): - params = json.dumps({'id': object_id}) - response = self.app.post('/api/action/dataset_follower_count', + def test_04_follower_count_bad_id(self): + for action in ('user_follower_count', 'dataset_follower_count', + 'group_follower_count'): + for object_id in ('bad id', ' ', 3, 35.7, 'xxx', ''): + params = json.dumps({'id': object_id}) + response = self.app.post('/api/action/{0}'.format(action), + params=params, status=409).json + assert response['success'] is False + assert 'id' in response['error'] + + def test_04_follower_count_missing_id(self): + for action in ('user_follower_count', 'dataset_follower_count', + 'group_follower_count'): + params = json.dumps({}) + response = self.app.post('/api/action/{0}'.format(action), params=params, status=409).json assert response['success'] is False - assert response['error'].has_key('id') - - def test_04_user_follower_count_missing_id(self): - params = json.dumps({}) - response = self.app.post('/api/action/user_follower_count', - params=params, status=409).json - assert response['success'] is False - assert response['error']['id'] == ['Missing value'] - - def test_04_dataset_follower_count_missing_id(self): - params = json.dumps({}) - response = self.app.post('/api/action/dataset_follower_count', - params=params, status=409).json - assert response['success'] is False - assert response['error']['id'] == ['Missing value'] + assert response['error']['id'] == ['Missing value'] def test_04_user_follower_count_no_followers(self): params = json.dumps({'id': self.annafan.id}) @@ -386,35 +522,31 @@ def test_04_dataset_follower_count_no_followers(self): assert response['success'] is True assert response['result'] == 0 - def test_04_user_follower_list_bad_id(self): - for object_id in ('bad id', ' ', 3, 35.7, 'xxx', ''): - params = json.dumps({'id': object_id}) - response = self.app.post('/api/action/user_follower_list', - params=params, status=409).json - assert response['success'] is False - assert response['error']['id'] + def test_04_group_follower_count_no_followers(self): + params = json.dumps({'id': self.davids_group.id}) + response = self.app.post('/api/action/group_follower_count', + params=params).json + assert response['success'] is True + assert response['result'] == 0 - def test_04_dataset_follower_list_bad_id(self): - for object_id in ('bad id', ' ', 3, 35.7, 'xxx', ''): - params = json.dumps({'id': object_id}) - response = self.app.post('/api/action/dataset_follower_list', + def test_04_follower_list_bad_id(self): + for action in ('user_follower_list', 'dataset_follower_list', + 'group_follower_list'): + for object_id in ('bad id', ' ', 3, 35.7, 'xxx', ''): + params = json.dumps({'id': object_id}) + response = self.app.post('/api/action/{0}'.format(action), + params=params, status=409).json + assert response['success'] is False + assert response['error']['id'] + + def test_04_follower_list_missing_id(self): + for action in ('user_follower_list', 'dataset_follower_list', + 'group_follower_list'): + params = json.dumps({}) + response = self.app.post('/api/action/{0}'.format(action), params=params, status=409).json assert response['success'] is False - assert response['error']['id'] - - def test_04_user_follower_list_missing_id(self): - params = json.dumps({}) - response = self.app.post('/api/action/user_follower_list', - params=params, status=409).json - assert response['success'] is False - assert response['error']['id'] == ['Missing value'] - - def test_04_dataset_follower_list_missing_id(self): - params = json.dumps({}) - response = self.app.post('/api/action/dataset_follower_list', - params=params, status=409).json - assert response['success'] is False - assert response['error']['id'] == ['Missing value'] + assert response['error']['id'] == ['Missing value'] def test_04_user_follower_list_no_followers(self): params = json.dumps({'id': self.annafan.id}) @@ -430,27 +562,39 @@ def test_04_dataset_follower_list_no_followers(self): assert response['success'] is True assert response['result'] == [] - def test_04_am_following_dataset_bad_id(self): - for object_id in ('bad id', ' ', 3, 35.7, 'xxx'): - params = json.dumps({'id': object_id}) - extra_environ = {'Authorization': str(self.annafan.apikey)} - response = self.app.post('/api/action/am_following_dataset', - params=params, extra_environ=extra_environ, - status=409).json - assert response['success'] is False - assert response['error']['id'] == [u'Not found: Dataset'] - - def test_04_am_following_dataset_missing_id(self): - for id in ('missing', None, ''): - if id == 'missing': - params = json.dumps({}) - else: - params = json.dumps({'id':id}) - extra_environ = {'Authorization': str(self.annafan.apikey)} - response = self.app.post('/api/action/am_following_dataset', - params=params, extra_environ=extra_environ, status=409).json - assert response['success'] is False - assert response['error']['id'] == [u'Missing value'] + def test_04_group_follower_list_no_followers(self): + params = json.dumps({'id': self.davids_group.id}) + response = self.app.post('/api/action/group_follower_list', + params=params).json + assert response['success'] is True + assert response['result'] == [] + + def test_04_am_following_bad_id(self): + for action in ('am_following_dataset', 'am_following_user', + 'am_following_group'): + for object_id in ('bad id', ' ', 3, 35.7, 'xxx'): + params = json.dumps({'id': object_id}) + extra_environ = {'Authorization': str(self.annafan.apikey)} + response = self.app.post('/api/action/{0}'.format(action), + params=params, extra_environ=extra_environ, + status=409).json + assert response['success'] is False + assert response['error']['id'][0].startswith('Not found: ') + + def test_04_am_following_missing_id(self): + for action in ('am_following_dataset', 'am_following_user', + 'am_following_group'): + for id in ('missing', None, ''): + if id == 'missing': + params = json.dumps({}) + else: + params = json.dumps({'id':id}) + extra_environ = {'Authorization': str(self.annafan.apikey)} + response = self.app.post('/api/action/{0}'.format(action), + params=params, extra_environ=extra_environ, + status=409).json + assert response['success'] is False + assert response['error']['id'] == [u'Missing value'] def test_04_am_following_dataset_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): @@ -468,28 +612,6 @@ def test_04_am_following_dataset_missing_apikey(self): assert response['success'] == False assert response['error']['message'] == 'Access denied' - def test_04_am_following_user_bad_id(self): - for object_id in ('bad id', ' ', 3, 35.7, 'xxx'): - params = json.dumps({'id': object_id}) - extra_environ = {'Authorization': str(self.annafan.apikey)} - response = self.app.post('/api/action/am_following_user', - params=params, extra_environ=extra_environ, - status=409).json - assert response['success'] is False - assert response['error']['id'] == [u'Not found: User'] - - def test_04_am_following_user_missing_id(self): - for id in ('missing', None, ''): - if id == 'missing': - params = json.dumps({}) - else: - params = json.dumps({'id':id}) - extra_environ = {'Authorization': str(self.annafan.apikey)} - response = self.app.post('/api/action/am_following_user', - params=params, extra_environ=extra_environ, status=409).json - assert response['success'] is False - assert response['error']['id'] == [u'Missing value'] - def test_04_am_following_user_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): params = json.dumps({'id': self.annafan.id}) @@ -506,8 +628,25 @@ def test_04_am_following_user_missing_apikey(self): assert response['success'] == False assert response['error']['message'] == 'Access denied' + def test_04_am_following_group_bad_apikey(self): + for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): + params = json.dumps({'id': self.rogers_group.id}) + extra_environ = {'Authorization': apikey} + response = self.app.post('/api/action/am_following_group', + params=params, extra_environ=extra_environ, status=403).json + assert response['success'] == False + assert response['error']['message'] == 'Access denied' + + def test_04_am_following_group_missing_apikey(self): + params = json.dumps({'id': self.rogers_group.id}) + response = self.app.post('/api/action/am_following_group', + params=params, status=403).json + assert response['success'] == False + assert response['error']['message'] == 'Access denied' + + class TestFollowerDelete(object): - '''Tests for the unfollow_user and unfollow_dataset APIs.''' + '''Tests for the unfollow_* APIs.''' @classmethod def setup_class(self): @@ -519,6 +658,8 @@ def setup_class(self): self.joeadmin = ckan.model.User.get('joeadmin') self.warandpeace = ckan.model.Package.get('warandpeace') self.annakarenina = ckan.model.Package.get('annakarenina') + self.rogers_group = ckan.model.Group.get('roger') + self.davids_group = ckan.model.Group.get('david') self.app = paste.fixture.TestApp(pylons.test.pylonsapp) follow_user(self.app, self.testsysadmin.id, self.testsysadmin.apikey, self.joeadmin.id, self.joeadmin.id) @@ -539,6 +680,8 @@ def setup_class(self): self.warandpeace.id, self.warandpeace.id) follow_dataset(self.app, self.annafan.id, self.annafan.apikey, self.warandpeace.id, self.warandpeace.id) + follow_group(self.app, self.annafan.id, self.annafan.apikey, + self.davids_group.id, self.davids_group.id) @classmethod def teardown_class(self): @@ -574,111 +717,81 @@ def test_01_unfollow_dataset_not_exists(self): assert response['error']['message'].startswith( 'Not found: You are not following ') - def test_01_unfollow_user_bad_apikey(self): - '''Test the error response when a user tries to unfollow a user - but provides a bad API key. + def test_01_unfollow_group_not_exists(self): + '''Test the error response when a user tries to unfollow a group that + she is not following. ''' - for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): - params = json.dumps({ - 'id': self.joeadmin.id, - }) - extra_environ = { - 'Authorization': apikey, - } - response = self.app.post('/api/action/unfollow_user', - params=params, extra_environ=extra_environ, status=403).json - assert response['success'] == False - assert response['error']['message'] == 'Access denied' + params = json.dumps({'id': self.rogers_group.id}) + extra_environ = { + 'Authorization': str(self.annafan.apikey), + } + response = self.app.post('/api/action/unfollow_group', + params=params, extra_environ=extra_environ, status=404).json + assert response['success'] is False + assert response['error']['message'].startswith( + 'Not found: You are not following ') - def test_01_unfollow_dataset_bad_apikey(self): - '''Test the error response when a user tries to unfollow a dataset + def test_01_unfollow_bad_apikey(self): + '''Test the error response when a user tries to unfollow something but provides a bad API key. ''' - for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): - params = json.dumps({ - 'id': self.warandpeace.id, - }) - extra_environ = { - 'Authorization': apikey, - } - response = self.app.post('/api/action/unfollow_dataset', - params=params, extra_environ=extra_environ, status=403).json - assert response['success'] == False - assert response['error']['message'] == 'Access denied' - - def test_01_unfollow_user_missing_apikey(self): - params = json.dumps({ - 'id': self.joeadmin.id, - }) - response = self.app.post('/api/action/unfollow_user', - params=params, status=403).json - assert response['success'] == False - assert response['error']['message'] == 'Access denied' - - def test_01_unfollow_dataset_missing_apikey(self): - params = json.dumps({ - 'id': self.warandpeace.id, - }) - response = self.app.post('/api/action/unfollow_dataset', - params=params, status=403).json - assert response['success'] == False - assert response['error']['message'] == 'Access denied' - - def test_01_unfollow_user_bad_object_id(self): - '''Test error response when calling unfollow_user with a bad object - id. - - ''' - for object_id in ('bad id', ' ', 3, 35.7, 'xxx'): - params = json.dumps({ - 'id': object_id, - }) - extra_environ = { - 'Authorization': str(self.annafan.apikey), - } - response = self.app.post('/api/action/unfollow_user', - params=params, extra_environ=extra_environ, status=409).json - assert response['success'] == False - assert response['error']['id'] == [u'Not found: User'] - - def test_01_unfollow_dataset_bad_object_id(self): - '''Test error response when calling unfollow_dataset with a bad object - id. + for action in ('unfollow_user', 'unfollow_dataset', 'unfollow_group'): + for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', + 'xxx'): + params = json.dumps({ + 'id': self.joeadmin.id, + }) + extra_environ = { + 'Authorization': apikey, + } + response = self.app.post('/api/action/{0}'.format(action), + params=params, extra_environ=extra_environ, + status=(403,409)).json + assert response['success'] is False + assert response['error']['message'] == 'Access denied' - ''' - for object_id in ('bad id', ' ', 3, 35.7, 'xxx'): + def test_01_unfollow_missing_apikey(self): + '''Test error response when calling unfollow_* without api key.''' + for action in ('unfollow_user', 'unfollow_dataset', 'unfollow_group'): params = json.dumps({ - 'id': object_id, + 'id': self.joeadmin.id, }) - extra_environ = { - 'Authorization': str(self.annafan.apikey), - } - response = self.app.post('/api/action/unfollow_dataset', - params=params, extra_environ=extra_environ, status=409).json - assert response['success'] == False - assert response['error']['id'] == [u'Not found: Dataset'] - - def test_01_unfollow_user_missing_object_id(self): - for id in ('missing', None, ''): - if id == 'missing': - params = json.dumps({}) - else: - params = json.dumps({'id':id}) - extra_environ = {'Authorization': str(self.annafan.apikey),} - response = self.app.post('/api/action/unfollow_user', - params=params, extra_environ=extra_environ, status=409).json - assert response['success'] == False - assert response['error']['id'] == [u'Missing value'] + response = self.app.post('/api/action/{0}'.format(action), + params=params, status=403).json + assert response['success'] is False + assert response['error']['message'] == 'Access denied' - def test_01_unfollow_dataset_missing_object_id(self): - params = json.dumps({}) - extra_environ = {'Authorization': str(self.annafan.apikey),} - response = self.app.post('/api/action/unfollow_dataset', - params=params, extra_environ=extra_environ, status=409).json - assert response['success'] == False - assert response['error']['id'] == ['Missing value'] + def test_01_unfollow_bad_object_id(self): + '''Test error response when calling unfollow_* with bad object id.''' + for action in ('unfollow_user', 'unfollow_dataset', 'unfollow_group'): + for object_id in ('bad id', ' ', 3, 35.7, 'xxx'): + params = json.dumps({ + 'id': object_id, + }) + extra_environ = { + 'Authorization': str(self.annafan.apikey), + } + response = self.app.post('/api/action/{0}'.format(action), + params=params, extra_environ=extra_environ, + status=409).json + assert response['success'] is False + assert response['error']['id'][0].startswith('Not found') + + def test_01_unfollow_missing_object_id(self): + for action in ('unfollow_user', 'unfollow_dataset', 'unfollow_group'): + for id in ('missing', None, ''): + if id == 'missing': + params = json.dumps({}) + else: + params = json.dumps({'id': id}) + extra_environ = {'Authorization': str(self.annafan.apikey)} + response = self.app.post('/api/action/{0}'.format(action), + params=params, extra_environ=extra_environ, + status=409).json + assert response['success'] is False + assert response['error']['id'] == [u'Missing value'] def _unfollow_user(self, follower_id, apikey, object_id, object_arg): '''Test a user unfollowing a user via the API. @@ -799,11 +912,73 @@ def _unfollow_dataset(self, user_id, apikey, dataset_id, dataset_arg): assert response['success'] is True assert response['result'] == count_before - 1 + def _unfollow_group(self, user_id, apikey, group_id, group_arg): + '''Test a user unfollowing a group via the API. + + :param user_id: id of the user + :param apikey: API key of the user + :param group_id: id of the group + :param group_arg: the argument to pass to unfollow_group as the id + of the group, could be the group's id or name. + + ''' + # Record the group's number of followers before. + params = json.dumps({'id': group_id}) + response = self.app.post('/api/action/group_follower_count', + params=params).json + assert response['success'] is True + count_before = response['result'] + + # Check that the user is following the group. + params = json.dumps({'id': group_id}) + extra_environ = {'Authorization': str(apikey)} + response = self.app.post('/api/action/am_following_group', + params=params, extra_environ=extra_environ).json + assert response['success'] is True + assert response['result'] is True + + # Make the user unfollow the group. + params = { + 'id': group_arg, + } + extra_environ = {'Authorization': str(apikey)} + response = self.app.post('/api/action/unfollow_group', + params=json.dumps(params), extra_environ=extra_environ).json + assert response['success'] is True + + # Check that am_following_group now returns False. + params = json.dumps({'id': group_id}) + extra_environ = {'Authorization': str(apikey)} + response = self.app.post('/api/action/am_following_group', + params=params, extra_environ=extra_environ).json + assert response['success'] is True + assert response['result'] is False + + # Check that the user doesn't appear in the group's list of + # followers. + params = json.dumps({'id': group_id}) + response = self.app.post('/api/action/group_follower_list', + params=params).json + assert response['success'] is True + assert 'result' in response + followers = response['result'] + assert len([follower for follower in followers if follower['id'] == + user_id]) == 0 + + # Check that the group's follower count has decreased by 1. + params = json.dumps({'id': group_id}) + response = self.app.post('/api/action/group_follower_count', + params=params).json + assert response['success'] is True + assert response['result'] == 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) self._unfollow_dataset(self.annafan.id, self.annafan.apikey, self.warandpeace.id, self.warandpeace.id) + self._unfollow_group(self.annafan.id, self.annafan.apikey, + self.davids_group.id, self.davids_group.id) class TestFollowerCascade(object): '''Tests for on delete cascade of follower table rows.''' From ee9eb3d98374b40c310459bb30216be48744896f Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Wed, 24 Oct 2012 14:15:43 +0200 Subject: [PATCH 03/14] [#3005] Remove retained model objects from test_follow.py This removes the AttributeDict hack introduced in the previous commit (21429c6) and replaces retained model objects in the test module with dicts so we don't DetachedInstanceErrors from SQLAlchemy. --- ckan/tests/functional/api/test_follow.py | 371 +++++++++++++---------- 1 file changed, 215 insertions(+), 156 deletions(-) diff --git a/ckan/tests/functional/api/test_follow.py b/ckan/tests/functional/api/test_follow.py index 05ecf4c8140..99c1e6d1666 100644 --- a/ckan/tests/functional/api/test_follow.py +++ b/ckan/tests/functional/api/test_follow.py @@ -283,56 +283,48 @@ def follow_group(app, user_id, apikey, group_id, group_arg): assert response['result'] == followee_count_before + 1 -class AttributeDict(dict): - def __getattr__(self, attr): - return self[attr] - - def __setattr__(self, attr, value): - self[attr] = value - - class TestFollow(object): '''Tests for the follower API.''' @classmethod def setup_class(self): ckan.tests.CreateTestData.create() - self.testsysadmin = AttributeDict( - id = ckan.model.User.get('testsysadmin').id, - apikey = ckan.model.User.get('testsysadmin').apikey, - name = ckan.model.User.get('testsysadmin').name, - ) - self.annafan = AttributeDict({ + self.testsysadmin = { + 'id': ckan.model.User.get('testsysadmin').id, + 'apikey': ckan.model.User.get('testsysadmin').apikey, + 'name': ckan.model.User.get('testsysadmin').name, + } + self.annafan = { 'id': ckan.model.User.get('annafan').id, 'apikey': ckan.model.User.get('annafan').apikey, 'name': ckan.model.User.get('annafan').name, - }) - self.russianfan = AttributeDict({ + } + self.russianfan = { 'id': ckan.model.User.get('russianfan').id, 'apikey': ckan.model.User.get('russianfan').apikey, 'name': ckan.model.User.get('russianfan').name, - }) - self.joeadmin = AttributeDict({ + } + self.joeadmin = { 'id': ckan.model.User.get('joeadmin').id, 'apikey': ckan.model.User.get('joeadmin').apikey, 'name': ckan.model.User.get('joeadmin').name, - }) - self.warandpeace = AttributeDict({ + } + self.warandpeace = { 'id': ckan.model.Package.get('warandpeace').id, 'name': ckan.model.Package.get('warandpeace').name, - }) - self.annakarenina = AttributeDict({ + } + self.annakarenina = { 'id': ckan.model.Package.get('annakarenina').id, 'name': ckan.model.Package.get('annakarenina').name, - }) - self.rogers_group = AttributeDict({ + } + self.rogers_group = { 'id': ckan.model.Group.get('roger').id, 'name': ckan.model.Group.get('roger').name, - }) - self.davids_group = AttributeDict({ + } + self.davids_group = { 'id': ckan.model.Group.get('david').id, 'name': ckan.model.Group.get('david').name, - }) + } self.app = paste.fixture.TestApp(pylons.test.pylonsapp) @classmethod @@ -341,7 +333,7 @@ def teardown_class(self): def test_01_user_follow_user_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): - params = json.dumps({'id': self.russianfan.id}) + params = json.dumps({'id': self.russianfan['id']}) extra_environ = {'Authorization': apikey} response = self.app.post('/api/action/follow_user', params=params, extra_environ=extra_environ, status=403).json @@ -350,7 +342,7 @@ def test_01_user_follow_user_bad_apikey(self): def test_01_user_follow_dataset_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): - params = json.dumps({'id': self.warandpeace.id}) + params = json.dumps({'id': self.warandpeace['id']}) extra_environ = {'Authorization': apikey} response = self.app.post('/api/action/follow_dataset', params=params, extra_environ=extra_environ, status=403).json @@ -359,7 +351,7 @@ def test_01_user_follow_dataset_bad_apikey(self): def test_01_user_follow_group_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): - params = json.dumps({'id': self.rogers_group.id}) + params = json.dumps({'id': self.rogers_group['id']}) extra_environ = {'Authorization': apikey} response = self.app.post('/api/action/follow_group', params=params, extra_environ=extra_environ, status=403).json @@ -367,21 +359,21 @@ def test_01_user_follow_group_bad_apikey(self): assert response['error']['message'] == 'Access denied' def test_01_user_follow_user_missing_apikey(self): - params = json.dumps({'id': self.russianfan.id}) + params = json.dumps({'id': self.russianfan['id']}) response = self.app.post('/api/action/follow_user', params=params, status=403).json assert response['success'] is False assert response['error']['message'] == 'Access denied' def test_01_user_follow_dataset_missing_apikey(self): - params = json.dumps({'id': self.warandpeace.id}) + params = json.dumps({'id': self.warandpeace['id']}) response = self.app.post('/api/action/follow_dataset', params=params, status=403).json assert response['success'] is False assert response['error']['message'] == 'Access denied' def test_01_user_follow_group_missing_apikey(self): - params = json.dumps({'id': self.rogers_group.id}) + params = json.dumps({'id': self.rogers_group['id']}) response = self.app.post('/api/action/follow_group', params=params, status=403).json assert response['success'] is False @@ -391,7 +383,7 @@ def test_01_follow_bad_object_id(self): for action in ('follow_user', 'follow_dataset', 'follow_group'): for object_id in ('bad id', ' ', 3, 35.7, 'xxx'): params = json.dumps({'id': object_id}) - extra_environ = {'Authorization': str(self.annafan.apikey)} + extra_environ = {'Authorization': str(self.annafan['apikey'])} response = self.app.post('/api/action/{0}'.format(action), params=params, extra_environ=extra_environ, status=409).json @@ -402,7 +394,7 @@ def test_01_follow_empty_object_id(self): for action in ('follow_user', 'follow_dataset', 'follow_group'): for object_id in ('', None): params = json.dumps({'id': object_id}) - extra_environ = {'Authorization': str(self.annafan.apikey)} + extra_environ = {'Authorization': str(self.annafan['apikey'])} response = self.app.post('/api/action/{0}'.format(action), params=params, extra_environ=extra_environ, status=409).json @@ -412,42 +404,42 @@ def test_01_follow_empty_object_id(self): def test_01_follow_missing_object_id(self): for action in ('follow_user', 'follow_dataset', 'follow_group'): params = json.dumps({}) - extra_environ = {'Authorization': str(self.annafan.apikey)} + extra_environ = {'Authorization': str(self.annafan['apikey'])} response = self.app.post('/api/action/{0}'.format(action), params=params, extra_environ=extra_environ, status=409).json assert response['success'] is False assert response['error']['id'] == ['Missing value'] def test_02_user_follow_user_by_id(self): - follow_user(self.app, self.annafan.id, self.annafan.apikey, - self.russianfan.id, self.russianfan.id) + follow_user(self.app, self.annafan['id'], self.annafan['apikey'], + self.russianfan['id'], self.russianfan['id']) def test_02_user_follow_dataset_by_id(self): - follow_dataset(self.app, self.annafan.id, self.annafan.apikey, - self.warandpeace.id, self.warandpeace.id) + follow_dataset(self.app, self.annafan['id'], self.annafan['apikey'], + self.warandpeace['id'], self.warandpeace['id']) def test_02_user_follow_group_by_id(self): - follow_group(self.app, self.annafan.id, self.annafan.apikey, - self.rogers_group.id, self.rogers_group.id) + follow_group(self.app, self.annafan['id'], self.annafan['apikey'], + self.rogers_group['id'], self.rogers_group['id']) def test_02_user_follow_user_by_name(self): - follow_user(self.app, self.annafan.id, self.annafan.apikey, - self.testsysadmin.id, self.testsysadmin.name) + follow_user(self.app, self.annafan['id'], self.annafan['apikey'], + self.testsysadmin['id'], self.testsysadmin['name']) def test_02_user_follow_dataset_by_name(self): - follow_dataset(self.app, self.joeadmin.id, self.joeadmin.apikey, - self.warandpeace.id, self.warandpeace.name) + follow_dataset(self.app, self.joeadmin['id'], self.joeadmin['apikey'], + self.warandpeace['id'], self.warandpeace['name']) def test_02_user_follow_group_by_name(self): - follow_group(self.app, self.joeadmin.id, self.joeadmin.apikey, - self.rogers_group.id, self.rogers_group.name) + follow_group(self.app, self.joeadmin['id'], self.joeadmin['apikey'], + self.rogers_group['id'], self.rogers_group['name']) def test_03_user_follow_user_already_following(self): - for object_id in (self.russianfan.id, self.russianfan.name, - self.testsysadmin.id, self.testsysadmin.name): + for object_id in (self.russianfan['id'], self.russianfan['name'], + self.testsysadmin['id'], self.testsysadmin['name']): params = json.dumps({'id': object_id}) extra_environ = { - 'Authorization': str(self.annafan.apikey), + 'Authorization': str(self.annafan['apikey']), } response = self.app.post('/api/action/follow_user', params=params, extra_environ=extra_environ, status=409).json @@ -456,10 +448,10 @@ def test_03_user_follow_user_already_following(self): 'You are already following ') def test_03_user_follow_dataset_already_following(self): - for object_id in (self.warandpeace.id, self.warandpeace.name): + for object_id in (self.warandpeace['id'], self.warandpeace['name']): params = json.dumps({'id': object_id}) extra_environ = { - 'Authorization': str(self.annafan.apikey), + 'Authorization': str(self.annafan['apikey']), } response = self.app.post('/api/action/follow_dataset', params=params, extra_environ=extra_environ, status=409).json @@ -468,10 +460,10 @@ def test_03_user_follow_dataset_already_following(self): 'You are already following ') def test_03_user_follow_group_already_following(self): - for group_id in (self.rogers_group.id, self.rogers_group.name): + for group_id in (self.rogers_group['id'], self.rogers_group['name']): params = json.dumps({'id': group_id}) extra_environ = { - 'Authorization': str(self.annafan.apikey), + 'Authorization': str(self.annafan['apikey']), } response = self.app.post('/api/action/follow_group', params=params, extra_environ=extra_environ, status=409).json @@ -480,9 +472,9 @@ def test_03_user_follow_group_already_following(self): 'You are already following ') def test_03_user_cannot_follow_herself(self): - params = json.dumps({'id': self.annafan.id}) + params = json.dumps({'id': self.annafan['id']}) extra_environ = { - 'Authorization': str(self.annafan.apikey), + 'Authorization': str(self.annafan['apikey']), } response = self.app.post('/api/action/follow_user', params=params, extra_environ=extra_environ, status=409).json @@ -509,21 +501,21 @@ def test_04_follower_count_missing_id(self): assert response['error']['id'] == ['Missing value'] def test_04_user_follower_count_no_followers(self): - params = json.dumps({'id': self.annafan.id}) + params = json.dumps({'id': self.annafan['id']}) response = self.app.post('/api/action/user_follower_count', params=params).json assert response['success'] is True assert response['result'] == 0 def test_04_dataset_follower_count_no_followers(self): - params = json.dumps({'id': self.annakarenina.id}) + params = json.dumps({'id': self.annakarenina['id']}) response = self.app.post('/api/action/dataset_follower_count', params=params).json assert response['success'] is True assert response['result'] == 0 def test_04_group_follower_count_no_followers(self): - params = json.dumps({'id': self.davids_group.id}) + params = json.dumps({'id': self.davids_group['id']}) response = self.app.post('/api/action/group_follower_count', params=params).json assert response['success'] is True @@ -549,21 +541,21 @@ def test_04_follower_list_missing_id(self): assert response['error']['id'] == ['Missing value'] def test_04_user_follower_list_no_followers(self): - params = json.dumps({'id': self.annafan.id}) + params = json.dumps({'id': self.annafan['id']}) response = self.app.post('/api/action/user_follower_list', params=params).json assert response['success'] is True assert response['result'] == [] def test_04_dataset_follower_list_no_followers(self): - params = json.dumps({'id': self.annakarenina.id}) + params = json.dumps({'id': self.annakarenina['id']}) response = self.app.post('/api/action/dataset_follower_list', params=params).json assert response['success'] is True assert response['result'] == [] def test_04_group_follower_list_no_followers(self): - params = json.dumps({'id': self.davids_group.id}) + params = json.dumps({'id': self.davids_group['id']}) response = self.app.post('/api/action/group_follower_list', params=params).json assert response['success'] is True @@ -574,7 +566,7 @@ def test_04_am_following_bad_id(self): 'am_following_group'): for object_id in ('bad id', ' ', 3, 35.7, 'xxx'): params = json.dumps({'id': object_id}) - extra_environ = {'Authorization': str(self.annafan.apikey)} + extra_environ = {'Authorization': str(self.annafan['apikey'])} response = self.app.post('/api/action/{0}'.format(action), params=params, extra_environ=extra_environ, status=409).json @@ -589,7 +581,7 @@ def test_04_am_following_missing_id(self): params = json.dumps({}) else: params = json.dumps({'id':id}) - extra_environ = {'Authorization': str(self.annafan.apikey)} + extra_environ = {'Authorization': str(self.annafan['apikey'])} response = self.app.post('/api/action/{0}'.format(action), params=params, extra_environ=extra_environ, status=409).json @@ -598,7 +590,7 @@ def test_04_am_following_missing_id(self): def test_04_am_following_dataset_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): - params = json.dumps({'id': self.warandpeace.id}) + params = json.dumps({'id': self.warandpeace['id']}) extra_environ = {'Authorization': apikey} response = self.app.post('/api/action/am_following_dataset', params=params, extra_environ=extra_environ, status=403).json @@ -606,7 +598,7 @@ def test_04_am_following_dataset_bad_apikey(self): assert response['error']['message'] == 'Access denied' def test_04_am_following_dataset_missing_apikey(self): - params = json.dumps({'id': self.warandpeace.id}) + params = json.dumps({'id': self.warandpeace['id']}) response = self.app.post('/api/action/am_following_dataset', params=params, status=403).json assert response['success'] == False @@ -614,7 +606,7 @@ def test_04_am_following_dataset_missing_apikey(self): def test_04_am_following_user_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): - params = json.dumps({'id': self.annafan.id}) + params = json.dumps({'id': self.annafan['id']}) extra_environ = {'Authorization': apikey} response = self.app.post('/api/action/am_following_user', params=params, extra_environ=extra_environ, status=403).json @@ -622,7 +614,7 @@ def test_04_am_following_user_bad_apikey(self): assert response['error']['message'] == 'Access denied' def test_04_am_following_user_missing_apikey(self): - params = json.dumps({'id': self.annafan.id}) + params = json.dumps({'id': self.annafan['id']}) response = self.app.post('/api/action/am_following_user', params=params, status=403).json assert response['success'] == False @@ -630,7 +622,7 @@ def test_04_am_following_user_missing_apikey(self): def test_04_am_following_group_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): - params = json.dumps({'id': self.rogers_group.id}) + params = json.dumps({'id': self.rogers_group['id']}) extra_environ = {'Authorization': apikey} response = self.app.post('/api/action/am_following_group', params=params, extra_environ=extra_environ, status=403).json @@ -638,7 +630,7 @@ def test_04_am_following_group_bad_apikey(self): assert response['error']['message'] == 'Access denied' def test_04_am_following_group_missing_apikey(self): - params = json.dumps({'id': self.rogers_group.id}) + params = json.dumps({'id': self.rogers_group['id']}) response = self.app.post('/api/action/am_following_group', params=params, status=403).json assert response['success'] == False @@ -651,37 +643,70 @@ class TestFollowerDelete(object): @classmethod def setup_class(self): ckan.tests.CreateTestData.create() - self.testsysadmin = ckan.model.User.get('testsysadmin') - self.annafan = ckan.model.User.get('annafan') - self.russianfan = ckan.model.User.get('russianfan') - self.tester = ckan.model.User.get('tester') - self.joeadmin = ckan.model.User.get('joeadmin') - self.warandpeace = ckan.model.Package.get('warandpeace') - self.annakarenina = ckan.model.Package.get('annakarenina') - self.rogers_group = ckan.model.Group.get('roger') - self.davids_group = ckan.model.Group.get('david') + self.tester = { + 'id': ckan.model.User.get('tester').id, + 'apikey': ckan.model.User.get('tester').apikey, + 'name': ckan.model.User.get('tester').name, + } + self.testsysadmin = { + 'id': ckan.model.User.get('testsysadmin').id, + 'apikey': ckan.model.User.get('testsysadmin').apikey, + 'name': ckan.model.User.get('testsysadmin').name, + } + self.annafan = { + 'id': ckan.model.User.get('annafan').id, + 'apikey': ckan.model.User.get('annafan').apikey, + 'name': ckan.model.User.get('annafan').name, + } + self.russianfan = { + 'id': ckan.model.User.get('russianfan').id, + 'apikey': ckan.model.User.get('russianfan').apikey, + 'name': ckan.model.User.get('russianfan').name, + } + self.joeadmin = { + 'id': ckan.model.User.get('joeadmin').id, + 'apikey': ckan.model.User.get('joeadmin').apikey, + 'name': ckan.model.User.get('joeadmin').name, + } + self.warandpeace = { + 'id': ckan.model.Package.get('warandpeace').id, + 'name': ckan.model.Package.get('warandpeace').name, + } + self.annakarenina = { + 'id': ckan.model.Package.get('annakarenina').id, + 'name': ckan.model.Package.get('annakarenina').name, + } + self.rogers_group = { + 'id': ckan.model.Group.get('roger').id, + 'name': ckan.model.Group.get('roger').name, + } + self.davids_group = { + 'id': ckan.model.Group.get('david').id, + 'name': ckan.model.Group.get('david').name, + } self.app = paste.fixture.TestApp(pylons.test.pylonsapp) - follow_user(self.app, self.testsysadmin.id, self.testsysadmin.apikey, - self.joeadmin.id, self.joeadmin.id) - follow_user(self.app, self.tester.id, self.tester.apikey, - self.joeadmin.id, self.joeadmin.id) - follow_user(self.app, self.russianfan.id, self.russianfan.apikey, - self.joeadmin.id, self.joeadmin.id) - follow_user(self.app, self.annafan.id, self.annafan.apikey, - self.joeadmin.id, self.joeadmin.id) - follow_user(self.app, self.annafan.id, self.annafan.apikey, - self.tester.id, self.tester.id) - follow_dataset(self.app, self.testsysadmin.id, - self.testsysadmin.apikey, self.warandpeace.id, - self.warandpeace.id) - follow_dataset(self.app, self.tester.id, self.tester.apikey, - self.warandpeace.id, self.warandpeace.id) - follow_dataset(self.app, self.russianfan.id, self.russianfan.apikey, - self.warandpeace.id, self.warandpeace.id) - follow_dataset(self.app, self.annafan.id, self.annafan.apikey, - self.warandpeace.id, self.warandpeace.id) - follow_group(self.app, self.annafan.id, self.annafan.apikey, - self.davids_group.id, self.davids_group.id) + follow_user(self.app, self.testsysadmin['id'], + self.testsysadmin['apikey'], self.joeadmin['id'], + self.joeadmin['id']) + follow_user(self.app, self.tester['id'], self.tester['apikey'], + self.joeadmin['id'], self.joeadmin['id']) + follow_user(self.app, self.russianfan['id'], self.russianfan['apikey'], + self.joeadmin['id'], self.joeadmin['id']) + follow_user(self.app, self.annafan['id'], self.annafan['apikey'], + self.joeadmin['id'], self.joeadmin['id']) + follow_user(self.app, self.annafan['id'], self.annafan['apikey'], + self.tester['id'], self.tester['id']) + follow_dataset(self.app, self.testsysadmin['id'], + self.testsysadmin['apikey'], self.warandpeace['id'], + self.warandpeace['id']) + follow_dataset(self.app, self.tester['id'], self.tester['apikey'], + self.warandpeace['id'], self.warandpeace['id']) + follow_dataset(self.app, self.russianfan['id'], self.russianfan['apikey'], + self.warandpeace['id'], self.warandpeace['id']) + follow_dataset(self.app, self.annafan['id'], self.annafan['apikey'], + self.warandpeace['id'], self.warandpeace['id']) + follow_group(self.app, self.annafan['id'], self.annafan['apikey'], + self.davids_group['id'], self.davids_group['id']) @classmethod def teardown_class(self): @@ -692,9 +717,9 @@ def test_01_unfollow_user_not_exists(self): she is not following. ''' - params = json.dumps({'id': self.russianfan.id}) + params = json.dumps({'id': self.russianfan['id']}) extra_environ = { - 'Authorization': str(self.annafan.apikey), + 'Authorization': str(self.annafan['apikey']), } response = self.app.post('/api/action/unfollow_user', params=params, extra_environ=extra_environ, status=404).json @@ -707,9 +732,9 @@ def test_01_unfollow_dataset_not_exists(self): she is not following. ''' - params = json.dumps({'id': self.annakarenina.id}) + params = json.dumps({'id': self.annakarenina['id']}) extra_environ = { - 'Authorization': str(self.annafan.apikey), + 'Authorization': str(self.annafan['apikey']), } response = self.app.post('/api/action/unfollow_dataset', params=params, extra_environ=extra_environ, status=404).json @@ -722,9 +747,9 @@ def test_01_unfollow_group_not_exists(self): she is not following. ''' - params = json.dumps({'id': self.rogers_group.id}) + params = json.dumps({'id': self.rogers_group['id']}) extra_environ = { - 'Authorization': str(self.annafan.apikey), + 'Authorization': str(self.annafan['apikey']), } response = self.app.post('/api/action/unfollow_group', params=params, extra_environ=extra_environ, status=404).json @@ -741,7 +766,7 @@ def test_01_unfollow_bad_apikey(self): for apikey in ('bad api key', '', ' ', 'None', '3', '35.7', 'xxx'): params = json.dumps({ - 'id': self.joeadmin.id, + 'id': self.joeadmin['id'], }) extra_environ = { 'Authorization': apikey, @@ -756,7 +781,7 @@ def test_01_unfollow_missing_apikey(self): '''Test error response when calling unfollow_* without api key.''' for action in ('unfollow_user', 'unfollow_dataset', 'unfollow_group'): params = json.dumps({ - 'id': self.joeadmin.id, + 'id': self.joeadmin['id'], }) response = self.app.post('/api/action/{0}'.format(action), params=params, status=403).json @@ -771,7 +796,7 @@ def test_01_unfollow_bad_object_id(self): 'id': object_id, }) extra_environ = { - 'Authorization': str(self.annafan.apikey), + 'Authorization': str(self.annafan['apikey']), } response = self.app.post('/api/action/{0}'.format(action), params=params, extra_environ=extra_environ, @@ -786,7 +811,7 @@ def test_01_unfollow_missing_object_id(self): params = json.dumps({}) else: params = json.dumps({'id': id}) - extra_environ = {'Authorization': str(self.annafan.apikey)} + extra_environ = {'Authorization': str(self.annafan['apikey'])} response = self.app.post('/api/action/{0}'.format(action), params=params, extra_environ=extra_environ, status=409).json @@ -973,12 +998,12 @@ def _unfollow_group(self, user_id, apikey, group_id, group_arg): assert response['result'] == 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) - self._unfollow_dataset(self.annafan.id, self.annafan.apikey, - self.warandpeace.id, self.warandpeace.id) - self._unfollow_group(self.annafan.id, self.annafan.apikey, - self.davids_group.id, self.davids_group.id) + self._unfollow_user(self.annafan['id'], self.annafan['apikey'], + self.joeadmin['id'], self.joeadmin['id']) + self._unfollow_dataset(self.annafan['id'], self.annafan['apikey'], + self.warandpeace['id'], self.warandpeace['id']) + self._unfollow_group(self.annafan['id'], self.annafan['apikey'], + self.davids_group['id'], self.davids_group['id']) class TestFollowerCascade(object): '''Tests for on delete cascade of follower table rows.''' @@ -986,43 +1011,77 @@ class TestFollowerCascade(object): @classmethod def setup_class(self): ckan.tests.CreateTestData.create() - self.testsysadmin = ckan.model.User.get('testsysadmin') - self.annafan = ckan.model.User.get('annafan') - self.russianfan = ckan.model.User.get('russianfan') - self.tester = ckan.model.User.get('tester') - self.joeadmin = ckan.model.User.get('joeadmin') - self.warandpeace = ckan.model.Package.get('warandpeace') - self.annakarenina = ckan.model.Package.get('annakarenina') + self.tester = { + 'id': ckan.model.User.get('tester').id, + 'apikey': ckan.model.User.get('tester').apikey, + 'name': ckan.model.User.get('tester').name, + } + self.testsysadmin = { + 'id': ckan.model.User.get('testsysadmin').id, + 'apikey': ckan.model.User.get('testsysadmin').apikey, + 'name': ckan.model.User.get('testsysadmin').name, + } + self.annafan = { + 'id': ckan.model.User.get('annafan').id, + 'apikey': ckan.model.User.get('annafan').apikey, + 'name': ckan.model.User.get('annafan').name, + } + self.russianfan = { + 'id': ckan.model.User.get('russianfan').id, + 'apikey': ckan.model.User.get('russianfan').apikey, + 'name': ckan.model.User.get('russianfan').name, + } + self.joeadmin = { + 'id': ckan.model.User.get('joeadmin').id, + 'apikey': ckan.model.User.get('joeadmin').apikey, + 'name': ckan.model.User.get('joeadmin').name, + } + self.warandpeace = { + 'id': ckan.model.Package.get('warandpeace').id, + 'name': ckan.model.Package.get('warandpeace').name, + } + self.annakarenina = { + 'id': ckan.model.Package.get('annakarenina').id, + 'name': ckan.model.Package.get('annakarenina').name, + } + self.rogers_group = { + 'id': ckan.model.Group.get('roger').id, + 'name': ckan.model.Group.get('roger').name, + } + self.davids_group = { + 'id': ckan.model.Group.get('david').id, + 'name': ckan.model.Group.get('david').name, + } self.app = paste.fixture.TestApp(pylons.test.pylonsapp) - follow_user(self.app, self.joeadmin.id, self.joeadmin.apikey, - self.testsysadmin.id, self.testsysadmin.id) + follow_user(self.app, self.joeadmin['id'], self.joeadmin['apikey'], + self.testsysadmin['id'], self.testsysadmin['id']) - follow_user(self.app, self.annafan.id, self.annafan.apikey, - self.testsysadmin.id, self.testsysadmin.id) - follow_user(self.app, self.russianfan.id, self.russianfan.apikey, - self.testsysadmin.id, self.testsysadmin.id) + follow_user(self.app, self.annafan['id'], self.annafan['apikey'], + self.testsysadmin['id'], self.testsysadmin['id']) + follow_user(self.app, self.russianfan['id'], self.russianfan['apikey'], + self.testsysadmin['id'], self.testsysadmin['id']) - follow_dataset(self.app, self.joeadmin.id, self.joeadmin.apikey, - self.annakarenina.id, self.annakarenina.id) + follow_dataset(self.app, self.joeadmin['id'], self.joeadmin['apikey'], + self.annakarenina['id'], self.annakarenina['id']) - follow_dataset(self.app, self.annafan.id, self.annafan.apikey, - self.annakarenina.id, self.annakarenina.id) - follow_dataset(self.app, self.russianfan.id, self.russianfan.apikey, - self.annakarenina.id, self.annakarenina.id) + follow_dataset(self.app, self.annafan['id'], self.annafan['apikey'], + self.annakarenina['id'], self.annakarenina['id']) + follow_dataset(self.app, self.russianfan['id'], self.russianfan['apikey'], + self.annakarenina['id'], self.annakarenina['id']) - follow_user(self.app, self.tester.id, self.tester.apikey, - self.joeadmin.id, self.joeadmin.id) + follow_user(self.app, self.tester['id'], self.tester['apikey'], + self.joeadmin['id'], self.joeadmin['id']) - follow_dataset(self.app, self.testsysadmin.id, - self.testsysadmin.apikey, self.warandpeace.id, - self.warandpeace.id) + follow_dataset(self.app, self.testsysadmin['id'], + self.testsysadmin['apikey'], self.warandpeace['id'], + self.warandpeace['id']) session = ckan.model.Session() - session.delete(self.joeadmin) + session.delete(ckan.model.User.get('joeadmin')) session.commit() - session.delete(self.warandpeace) + session.delete(ckan.model.Package.get('warandpeace')) session.commit() @classmethod @@ -1065,7 +1124,7 @@ def test_01_on_delete_cascade_api(self): # It should no longer be possible to get am_following for joeadmin. params = json.dumps({'id': 'joeadmin'}) - extra_environ = {'Authorization': str(self.testsysadmin.apikey)} + extra_environ = {'Authorization': str(self.testsysadmin['apikey'])} response = self.app.post('/api/action/am_following_user', params=params, extra_environ=extra_environ, status=409).json assert response['success'] is False @@ -1073,7 +1132,7 @@ def test_01_on_delete_cascade_api(self): # It should no longer be possible to get am_following for warandpeace. params = json.dumps({'id': 'warandpeace'}) - extra_environ = {'Authorization': str(self.testsysadmin.apikey)} + extra_environ = {'Authorization': str(self.testsysadmin['apikey'])} response = self.app.post('/api/action/am_following_dataset', params=params, extra_environ=extra_environ, status=409).json assert response['success'] is False @@ -1081,7 +1140,7 @@ def test_01_on_delete_cascade_api(self): # It should no longer be possible to unfollow joeadmin. params = json.dumps({'id': 'joeadmin'}) - extra_environ = {'Authorization': str(self.tester.apikey)} + extra_environ = {'Authorization': str(self.tester['apikey'])} response = self.app.post('/api/action/unfollow_user', params=params, extra_environ=extra_environ, status=409).json assert response['success'] is False @@ -1089,7 +1148,7 @@ def test_01_on_delete_cascade_api(self): # It should no longer be possible to unfollow warandpeace. params = json.dumps({'id': 'warandpeace'}) - extra_environ = {'Authorization': str(self.testsysadmin.apikey)} + extra_environ = {'Authorization': str(self.testsysadmin['apikey'])} response = self.app.post('/api/action/unfollow_dataset', params=params, extra_environ=extra_environ, status=409).json assert response['success'] is False @@ -1097,7 +1156,7 @@ def test_01_on_delete_cascade_api(self): # It should no longer be possible to follow joeadmin. params = json.dumps({'id': 'joeadmin'}) - extra_environ = {'Authorization': str(self.annafan.apikey)} + extra_environ = {'Authorization': str(self.annafan['apikey'])} response = self.app.post('/api/action/follow_user', params=params, extra_environ=extra_environ, status=409).json assert response['success'] is False @@ -1105,7 +1164,7 @@ def test_01_on_delete_cascade_api(self): # It should no longer be possible to follow warandpeace. params = json.dumps({'id': 'warandpeace'}) - extra_environ = {'Authorization': str(self.annafan.apikey)} + extra_environ = {'Authorization': str(self.annafan['apikey'])} response = self.app.post('/api/action/follow_dataset', params=params, extra_environ=extra_environ, status=409).json assert response['success'] is False @@ -1113,7 +1172,7 @@ def test_01_on_delete_cascade_api(self): # Users who joeadmin was following should no longer have him in their # follower list. - params = json.dumps({'id': self.testsysadmin.id}) + params = json.dumps({'id': self.testsysadmin['id']}) response = self.app.post('/api/action/user_follower_list', params=params).json assert response['success'] is True @@ -1122,7 +1181,7 @@ def test_01_on_delete_cascade_api(self): # Datasets who joeadmin was following should no longer have him in # their follower list. - params = json.dumps({'id': self.annakarenina.id}) + params = json.dumps({'id': self.annakarenina['id']}) response = self.app.post('/api/action/dataset_follower_list', params=params).json assert response['success'] is True @@ -1139,22 +1198,22 @@ def test_02_on_delete_cascade_db(self): session = ckan.model.Session() query = session.query(UserFollowingUser) - query = query.filter(UserFollowingUser.follower_id==self.joeadmin.id) + query = query.filter(UserFollowingUser.follower_id==self.joeadmin['id']) assert query.count() == 0 query = session.query(UserFollowingUser) - query = query.filter(UserFollowingUser.object_id==self.joeadmin.id) + query = query.filter(UserFollowingUser.object_id==self.joeadmin['id']) assert query.count() == 0 query = session.query(UserFollowingDataset) - query = query.filter(UserFollowingUser.follower_id==self.joeadmin.id) + query = query.filter(UserFollowingUser.follower_id==self.joeadmin['id']) assert query.count() == 0 # There should be no rows with warandpeace's id either. query = session.query(UserFollowingUser) - query = query.filter(UserFollowingUser.object_id==self.warandpeace.id) + query = query.filter(UserFollowingUser.object_id==self.warandpeace['id']) assert query.count() == 0 query = session.query(UserFollowingDataset) - query = query.filter(UserFollowingUser.object_id==self.warandpeace.id) + query = query.filter(UserFollowingUser.object_id==self.warandpeace['id']) assert query.count() == 0 From e55ca20ddf0068e7fb8d2a639cf406f14d0fa848 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Wed, 24 Oct 2012 14:37:26 +0200 Subject: [PATCH 04/14] [#3005] Add tests for group following on delete cascade --- ckan/tests/functional/api/test_follow.py | 62 +++++++++++++++++++++--- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/ckan/tests/functional/api/test_follow.py b/ckan/tests/functional/api/test_follow.py index 99c1e6d1666..ab0df832f48 100644 --- a/ckan/tests/functional/api/test_follow.py +++ b/ckan/tests/functional/api/test_follow.py @@ -1077,6 +1077,10 @@ def setup_class(self): self.testsysadmin['apikey'], self.warandpeace['id'], self.warandpeace['id']) + follow_group(self.app, self.testsysadmin['id'], + self.testsysadmin['apikey'], self.davids_group['id'], + self.davids_group['id']) + session = ckan.model.Session() session.delete(ckan.model.User.get('joeadmin')) session.commit() @@ -1084,6 +1088,9 @@ def setup_class(self): session.delete(ckan.model.Package.get('warandpeace')) session.commit() + session.delete(ckan.model.Group.get('david')) + session.commit() + @classmethod def teardown_class(self): ckan.model.repo.rebuild_db() @@ -1108,6 +1115,13 @@ def test_01_on_delete_cascade_api(self): assert response['success'] is False assert response['error'].has_key('id') + # It should no longer be possible to get david's follower list. + params = json.dumps({'id': 'david'}) + response = self.app.post('/api/action/group_follower_list', + params=params, status=409).json + assert response['success'] is False + assert response['error'].has_key('id') + # It should no longer be possible to get joeadmin's follower count. params = json.dumps({'id': 'joeadmin'}) response = self.app.post('/api/action/user_follower_count', @@ -1122,6 +1136,13 @@ def test_01_on_delete_cascade_api(self): assert response['success'] is False assert response['error'].has_key('id') + # It should no longer be possible to get david's follower count. + params = json.dumps({'id': 'david'}) + response = self.app.post('/api/action/group_follower_count', + params=params, status=409).json + assert response['success'] is False + assert response['error'].has_key('id') + # It should no longer be possible to get am_following for joeadmin. params = json.dumps({'id': 'joeadmin'}) extra_environ = {'Authorization': str(self.testsysadmin['apikey'])} @@ -1138,6 +1159,14 @@ def test_01_on_delete_cascade_api(self): assert response['success'] is False assert response['error'].has_key('id') + # It should no longer be possible to get am_following for david. + params = json.dumps({'id': 'david'}) + extra_environ = {'Authorization': str(self.testsysadmin['apikey'])} + response = self.app.post('/api/action/am_following_group', + params=params, extra_environ=extra_environ, status=409).json + assert response['success'] is False + assert response['error'].has_key('id') + # It should no longer be possible to unfollow joeadmin. params = json.dumps({'id': 'joeadmin'}) extra_environ = {'Authorization': str(self.tester['apikey'])} @@ -1154,6 +1183,14 @@ def test_01_on_delete_cascade_api(self): assert response['success'] is False assert response['error']['id'] == ['Not found: Dataset'] + # It should no longer be possible to unfollow david. + params = json.dumps({'id': 'david'}) + extra_environ = {'Authorization': str(self.testsysadmin['apikey'])} + response = self.app.post('/api/action/unfollow_group', + params=params, extra_environ=extra_environ, status=409).json + assert response['success'] is False + assert response['error']['id'] == ['Not found: Group'] + # It should no longer be possible to follow joeadmin. params = json.dumps({'id': 'joeadmin'}) extra_environ = {'Authorization': str(self.annafan['apikey'])} @@ -1170,6 +1207,14 @@ def test_01_on_delete_cascade_api(self): assert response['success'] is False assert response['error'].has_key('id') + # It should no longer be possible to follow david. + params = json.dumps({'id': 'david'}) + extra_environ = {'Authorization': str(self.annafan['apikey'])} + response = self.app.post('/api/action/follow_group', + params=params, extra_environ=extra_environ, status=409).json + assert response['success'] is False + assert response['error'].has_key('id') + # Users who joeadmin was following should no longer have him in their # follower list. params = json.dumps({'id': self.testsysadmin['id']}) @@ -1194,7 +1239,7 @@ def test_02_on_delete_cascade_db(self): # After the previous test above there should be no rows with joeadmin's # id in the UserFollowingUser or UserFollowingDataset tables. - from ckan.model import UserFollowingUser, UserFollowingDataset + from ckan.model import UserFollowingUser, UserFollowingDataset, UserFollowingGroup session = ckan.model.Session() query = session.query(UserFollowingUser) @@ -1209,11 +1254,16 @@ def test_02_on_delete_cascade_db(self): query = query.filter(UserFollowingUser.follower_id==self.joeadmin['id']) assert query.count() == 0 - # There should be no rows with warandpeace's id either. - query = session.query(UserFollowingUser) - query = query.filter(UserFollowingUser.object_id==self.warandpeace['id']) + # There should be no rows with warandpeace's id in the + # UserFollowingDataset table. + query = session.query(UserFollowingDataset) + query = query.filter( + UserFollowingDataset.object_id==self.warandpeace['id']) assert query.count() == 0 - query = session.query(UserFollowingDataset) - query = query.filter(UserFollowingUser.object_id==self.warandpeace['id']) + # There should be no rows with david's id in the + # UserFollowingGroup table. + query = session.query(UserFollowingGroup) + query = query.filter( + UserFollowingGroup.object_id==self.davids_group['id']) assert query.count() == 0 From bc3ac6a1b4fe5787ed2100021d8b215d317fd083 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Wed, 24 Oct 2012 16:22:45 +0200 Subject: [PATCH 05/14] [#3005] Add group follow, unfollow and followers pages These are not properly integrated into the frontend yet: no follow/unfollow buttons on the group pages, no link to group followers page, group followers page does not look right. But they work. --- ckan/config/routing.py | 5 ++- ckan/controllers/group.py | 50 +++++++++++++++++++++++++++++ ckan/templates/group/followers.html | 10 ++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 ckan/templates/group/followers.html diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 3558098be33..0b4f711d242 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -237,7 +237,10 @@ def make_map(): 'edit', 'authz', 'delete', - 'history' + 'history', + 'followers', + 'follow', + 'unfollow', ])) ) m.connect('group_read', '/group/{id}', action='read') diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 5a1dcdb7aa8..af7a3699c90 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -498,6 +498,56 @@ def history(self, id): return feed.writeString('utf-8') return render(self._history_template(c.group_dict['type'])) + def follow(self, id): + '''Start following this group.''' + context = {'model': model, + 'session': model.Session, + 'user': c.user or c.author} + data_dict = {'id': id} + try: + get_action('follow_group')(context, data_dict) + h.flash_success(_("You are now following {0}").format(id)) + except ValidationError as e: + error_message = (e.extra_msg or e.message or e.error_summary + or e.error_dict) + h.flash_error(error_message) + except NotAuthorized as e: + h.flash_error(e.extra_msg) + h.redirect_to(controller='group', action='read', id=id) + + def unfollow(self, id): + '''Stop following this group.''' + context = {'model': model, + 'session': model.Session, + 'user': c.user or c.author} + data_dict = {'id': id} + try: + get_action('unfollow_group')(context, data_dict) + h.flash_success(_("You are no longer following {0}").format(id)) + except ValidationError as e: + error_message = (e.extra_msg or e.message or e.error_summary + or e.error_dict) + h.flash_error(error_message) + except (NotFound, NotAuthorized) as e: + error_message = e.extra_msg or e.message + h.flash_error(error_message) + h.redirect_to(controller='group', action='read', id=id) + + def followers(self, id=None): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'for_view': True} + data_dict = {'id': id} + try: + c.group_dict = get_action('group_show')(context, data_dict) + c.followers = get_action('group_follower_list')(context, + {'id': c.group_dict['id']}) + except NotFound: + abort(404, _('Group not found')) + except NotAuthorized: + abort(401, _('Unauthorized to read group %s') % id) + + return render('group/followers.html') + def _render_edit_form(self, fs): # errors arrive in c.error and fs.errors c.fieldset = fs diff --git a/ckan/templates/group/followers.html b/ckan/templates/group/followers.html new file mode 100644 index 00000000000..521a1e0f87d --- /dev/null +++ b/ckan/templates/group/followers.html @@ -0,0 +1,10 @@ +{% extends "page.html" %} + +{% block subtitle %}{{ _('Followers') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} + +{% block primary_content %} +
+

{{ _('Followers') }}

+ {% snippet "user/snippets/followers.html", followers=c.followers %} +
+{% endblock %} From beb1542b3cbf41c147dcbde8bd47c6d4a69c7889 Mon Sep 17 00:00:00 2001 From: John Martin Date: Fri, 26 Oct 2012 09:44:25 +0100 Subject: [PATCH 06/14] Added UI for the following/unfollowing of groups --- ckan/config/routing.py | 1 + ckan/controllers/group.py | 17 ++++- ckan/lib/activity_streams.py | 7 +- ckan/lib/helpers.py | 4 +- .../javascript/modules/popover-context.js | 18 ++++- ckan/public/base/javascript/resource.config | 1 + ckan/public/base/less/activity.less | 1 + ckan/public/base/less/ckan.less | 1 - .../ajax_snippets/popover-context-group.html | 24 +++++++ ckan/templates/group/admins.html | 10 +++ ckan/templates/group/followers.html | 4 +- ckan/templates/group/read.html | 70 ++++++++++++++----- ckan/templates/user/read.html | 4 +- 13 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 ckan/templates/ajax_snippets/popover-context-group.html create mode 100644 ckan/templates/group/admins.html diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 0b4f711d242..9965a60a3a0 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -241,6 +241,7 @@ def make_map(): 'followers', 'follow', 'unfollow', + 'admins', ])) ) m.connect('group_read', '/group/{id}', action='read') diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index af7a3699c90..4499114efc4 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -129,8 +129,6 @@ def read(self, id): _("Cannot render description") c.description_formatted = genshi.HTML(error_msg) - c.group_admins = self.authorizer.get_admins(c.group) - context['return_query'] = True limit = 20 @@ -548,6 +546,21 @@ def followers(self, id=None): return render('group/followers.html') + def admins(self, id=None): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, + 'for_view': True} + data_dict = {'id': id} + try: + c.group_dict = get_action('group_show')(context, data_dict) + c.admins = self.authorizer.get_admins(context['group']) + except NotFound: + abort(404, _('Group not found')) + except NotAuthorized: + abort(401, _('Unauthorized to read group %s') % id) + + return render('group/admins.html') + def _render_edit_form(self, fs): # errors arrive in c.error and fs.errors c.fieldset = fs diff --git a/ckan/lib/activity_streams.py b/ckan/lib/activity_streams.py index 8d1d6fea515..9ac8eb9a887 100644 --- a/ckan/lib/activity_streams.py +++ b/ckan/lib/activity_streams.py @@ -32,7 +32,10 @@ def get_snippet_tag(activity, detail): return h.tag_link(detail['data']['tag']) def get_snippet_group(activity, detail): - return h.group_link(activity['data']['group']) + link = h.group_link(activity['data']['group']) + return literal('''%s''' + % (activity['object_id'], link) + ) def get_snippet_extra(activity, detail): return '"%s"' % detail['data']['package_extra']['key'] @@ -176,7 +179,7 @@ def activity_stream_string_new_related_item(): 'deleted related item': 'picture', 'follow dataset': 'sitemap', 'follow user': 'user', - 'follow group': 'groups', + 'follow group': 'group', 'new related item': 'picture', } diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 711040fa618..91c5a233b03 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -887,7 +887,7 @@ def tag_link(tag): def group_link(group): url = url_for(controller='group', action='read', id=group['name']) - return link_to(group['name'], url) + return link_to(group['title'], url) def dump_json(obj, **kw): @@ -963,7 +963,7 @@ def process_names(items): return items # these are the types of objects that can be followed -_follow_objects = ['dataset', 'user'] +_follow_objects = ['dataset', 'user', 'group'] def follow_button(obj_type, obj_id): diff --git a/ckan/public/base/javascript/modules/popover-context.js b/ckan/public/base/javascript/modules/popover-context.js index cdf8ebbc344..e44fafbf6dc 100644 --- a/ckan/public/base/javascript/modules/popover-context.js +++ b/ckan/public/base/javascript/modules/popover-context.js @@ -21,11 +21,13 @@ window.popover_context = { dict: { user: {}, - dataset: {} + dataset: {}, + group: {} }, render: { user: {}, - dataset: {} + dataset: {}, + group: {} } }; @@ -119,7 +121,10 @@ this.ckan.module('popover-context', function($, _) { var type = this.options.type; if (typeof window.popover_context.dict[type][id] == 'undefined') { var client = this.sandbox.client; - var endpoint = ( type == 'user' ) ? 'user_show' : 'package_show'; + var endpoint = type + '_show'; + if (type == 'dataset') { + endpoint = 'package_show'; + } client.call('GET', endpoint, '?id=' + id, this._onHandleData, this._onHandleError); } else { this._onHandleData(window.popover_context.dict[type][id]); @@ -180,6 +185,13 @@ this.ckan.module('popover-context', function($, _) { params.notes = raw.notes; params.num_resources = raw.resources.length; params.num_tags = raw.tags.length; + } 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.num_followers = raw.num_followers; } return params; }, diff --git a/ckan/public/base/javascript/resource.config b/ckan/public/base/javascript/resource.config index 6ab490b0633..8db348394b7 100644 --- a/ckan/public/base/javascript/resource.config +++ b/ckan/public/base/javascript/resource.config @@ -31,6 +31,7 @@ ckan = modules/data-viewer.js modules/resource-form.js modules/resource-upload-field.js + modules/follow.js modules/popover-context.js main = diff --git a/ckan/public/base/less/activity.less b/ckan/public/base/less/activity.less index 98a45a49eb7..dd082969542 100644 --- a/ckan/public/base/less/activity.less +++ b/ckan/public/base/less/activity.less @@ -77,4 +77,5 @@ &.follow-dataset i { background-color: @activityColorNeutral; } &.follow-user i { background-color: spin(@activityColorNeutral, 20); } &.new-related-item i { background-color: spin(@activityColorNew, -60); } + &.follow-group i { background-color: spin(@activityColorNew, -50); } } diff --git a/ckan/public/base/less/ckan.less b/ckan/public/base/less/ckan.less index bb837f804cb..21a8d6ab07e 100644 --- a/ckan/public/base/less/ckan.less +++ b/ckan/public/base/less/ckan.less @@ -14,7 +14,6 @@ @import "masthead.less"; @import "footer.less"; @import "profile.less"; -@import "disqus.less"; @import "activity.less"; @import "popover-context.less"; @import "follower-list.less"; diff --git a/ckan/templates/ajax_snippets/popover-context-group.html b/ckan/templates/ajax_snippets/popover-context-group.html new file mode 100644 index 00000000000..b0efe47f502 --- /dev/null +++ b/ckan/templates/ajax_snippets/popover-context-group.html @@ -0,0 +1,24 @@ +
+ {% if notes != 'null' %} +

+ {{ h.truncate(description, length=160, whole_word=True) }} +

+ {% endif %} +
+ {{ h.follow_button('group', id) }} + + + View Group + +
+
+
+
{{ _('Followers') }}
+
{{ num_followers }}
+
+
+
{{ _('Datasets') }}
+
{{ num_datasets }}
+
+
+
diff --git a/ckan/templates/group/admins.html b/ckan/templates/group/admins.html new file mode 100644 index 00000000000..28b480ac65d --- /dev/null +++ b/ckan/templates/group/admins.html @@ -0,0 +1,10 @@ +{% extends "group/read.html" %} + +{% block subtitle %}{{ _('Administrators') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} + +{% block primary_content_inner %} +
+

{{ _('Administrators') }}

+ {% snippet "user/snippets/followers.html", followers=c.admins %} +
+{% endblock %} diff --git a/ckan/templates/group/followers.html b/ckan/templates/group/followers.html index 521a1e0f87d..04089a4d68b 100644 --- a/ckan/templates/group/followers.html +++ b/ckan/templates/group/followers.html @@ -1,8 +1,8 @@ -{% extends "page.html" %} +{% extends "group/read.html" %} {% block subtitle %}{{ _('Followers') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} -{% block primary_content %} +{% block primary_content_inner %}

{{ _('Followers') }}

{% snippet "user/snippets/followers.html", followers=c.followers %} diff --git a/ckan/templates/group/read.html b/ckan/templates/group/read.html index 30160499aab..0405b3c67f5 100644 --- a/ckan/templates/group/read.html +++ b/ckan/templates/group/read.html @@ -8,33 +8,69 @@ {% endblock %} {% block actions_content %} - {% if h.check_access('group_update', {'id': c.group.id}) %} + {% if h.check_access('group_update', {'id': c.group_dict.id}) %}
  • {% link_for _('Add Dataset to Group'), controller='package', action='new', group=c.group_dict.id, class_='btn', icon='plus' %}
  • -
  • {% link_for _('Edit'), controller='group', action='edit', id=c.group_dict.name, class_='btn', icon='cog' %}
  • +
  • {% link_for _('Edit'), controller='group', action='edit', id=c.group_dict.name, class_='btn', icon='wrench' %}
  • {% endif %} - {#
  • {% link_for _('History'), controller='group', action='history', id=c.group_dict.name, class_='btn', icon='undo' %}
  • #} +
  • {{ h.follow_button('group', c.group_dict.id) }}
  • {% endblock %} {% block primary_content %}
    -
    - {% include "package/snippets/search_form.html" %} -
    - {{ c.page.pager(q=c.q) }} + {% block package_header %} + + {% endblock %} + {% block primary_content_inner %} +
    + {% include "package/snippets/search_form.html" %} +
    + {{ c.page.pager(q=c.q) }} + {% endblock %}
    {% endblock %} {% block secondary_content %} - {% snippet 'snippets/group.html', group=c.group_dict %} - -
    -

    {{ _('Administrators') }}

    - -
    +
    +
    +
    + + {{ c.group_dict.name }} + +
    +

    {{ c.group_dict.display_name }}

    + {% if c.group_dict.description %} + {% if truncate %} +

    {{ c.group_dict.description }}

    + {% else %} +

    {{ h.markdown_extract(c.group_dict.description, truncate) }}

    + {% endif %} + {% else %} +

    {{ _('There is no description for this group') }}

    + {% endif %} +
    +
    +
    {{ _('Followers') }}
    +
    {{ c.group_dict.num_followers }}
    +
    +
    +
    {{ _('Datasets') }}
    +
    {{ c.group_dict.packages|length }}
    +
    +
    +
    +
    {{ h.snippet('snippets/facet_list.html', title='Tags', name='tags', extras={'id':c.group_dict.id}) }} {{ h.snippet('snippets/facet_list.html', title='Formats', name='res_format', extras={'id':c.group_dict.id}) }} diff --git a/ckan/templates/user/read.html b/ckan/templates/user/read.html index 5b6923ce567..8b00ee5a9d1 100644 --- a/ckan/templates/user/read.html +++ b/ckan/templates/user/read.html @@ -74,11 +74,11 @@

    {{ user.fullname or _('No full name provided') }}

    {% endif %}
    -
    +
    {{ _('Followers') }}
    {{ user.num_followers }}
    -
    +
    {{ _('Datasets') }}
    {{ user.number_administered_packages }}
    From 88166e86e33887320e7ce2be12f6bd3dbc10cc47 Mon Sep 17 00:00:00 2001 From: John Martin Date: Mon, 5 Nov 2012 11:58:50 +0000 Subject: [PATCH 07/14] Fix after incorrect merge --- ckan/controllers/group.py | 65 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 7289268b6cc..2e76081764e 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -515,6 +515,71 @@ def activity(self, id): return render('group/activity_stream.html') + def follow(self, id): + '''Start following this group.''' + context = {'model': model, + 'session': model.Session, + 'user': c.user or c.author} + data_dict = {'id': id} + try: + get_action('follow_group')(context, data_dict) + h.flash_success(_("You are now following {0}").format(id)) + except ValidationError as e: + error_message = (e.extra_msg or e.message or e.error_summary + or e.error_dict) + h.flash_error(error_message) + except NotAuthorized as e: + h.flash_error(e.extra_msg) + h.redirect_to(controller='group', action='read', id=id) + + def unfollow(self, id): + '''Stop following this group.''' + context = {'model': model, + 'session': model.Session, + 'user': c.user or c.author} + data_dict = {'id': id} + try: + get_action('unfollow_group')(context, data_dict) + h.flash_success(_("You are no longer following {0}").format(id)) + except ValidationError as e: + error_message = (e.extra_msg or e.message or e.error_summary + or e.error_dict) + h.flash_error(error_message) + except (NotFound, NotAuthorized) as e: + error_message = e.extra_msg or e.message + h.flash_error(error_message) + h.redirect_to(controller='group', action='read', id=id) + + def followers(self, id=None): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'for_view': True} + data_dict = {'id': id} + try: + c.group_dict = get_action('group_show')(context, data_dict) + c.followers = get_action('group_follower_list')(context, + {'id': c.group_dict['id']}) + except NotFound: + abort(404, _('Group not found')) + except NotAuthorized: + abort(401, _('Unauthorized to read group %s') % id) + + return render('group/followers.html') + + def admins(self, id=None): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, + 'for_view': True} + data_dict = {'id': id} + try: + c.group_dict = get_action('group_show')(context, data_dict) + c.admins = self.authorizer.get_admins(context['group']) + except NotFound: + abort(404, _('Group not found')) + except NotAuthorized: + abort(401, _('Unauthorized to read group %s') % id) + + return render('group/admins.html') + def _render_edit_form(self, fs): # errors arrive in c.error and fs.errors c.fieldset = fs From eaef0364f32f18413dafe6a5d2a55338c06e0262 Mon Sep 17 00:00:00 2001 From: John Martin Date: Mon, 5 Nov 2012 11:59:04 +0000 Subject: [PATCH 08/14] Added activity stream to group front end --- ckan/templates/group/activity_stream.html | 10 ++++++---- ckan/templates/group/read.html | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ckan/templates/group/activity_stream.html b/ckan/templates/group/activity_stream.html index 8543d0d8a3a..fd106697cea 100644 --- a/ckan/templates/group/activity_stream.html +++ b/ckan/templates/group/activity_stream.html @@ -1,6 +1,8 @@ -{% extends "page.html" %} +{% extends "group/read.html" %} -{% block primary_content %} -

    {{ _('Activity Stream') }}

    - {{ c.group_activity_stream | safe }} +{% block primary_content_inner %} +
    +

    {{ _('Activity Stream') }}

    + {{ c.group_activity_stream | safe }} +
    {% endblock %} diff --git a/ckan/templates/group/read.html b/ckan/templates/group/read.html index 0405b3c67f5..edc5625a442 100644 --- a/ckan/templates/group/read.html +++ b/ckan/templates/group/read.html @@ -23,6 +23,9 @@ {% link_for _('Datasets'), controller='group', action='read', id=c.group_dict.name, icon='sitemap' %} + + {% link_for _('Activity Stream'), controller='group', action='activity', id=c.group_dict.name, icon='time' %} + {% link_for _('Followers'), controller='group', action='followers', id=c.group_dict.name, icon='group' %} From 35903c6477253962c3fb622399622601ebc248a0 Mon Sep 17 00:00:00 2001 From: John Martin Date: Mon, 5 Nov 2012 14:33:27 +0000 Subject: [PATCH 09/14] Added page title to the group activity stream page --- ckan/templates/group/activity_stream.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ckan/templates/group/activity_stream.html b/ckan/templates/group/activity_stream.html index fd106697cea..f14cdf76488 100644 --- a/ckan/templates/group/activity_stream.html +++ b/ckan/templates/group/activity_stream.html @@ -1,5 +1,7 @@ {% extends "group/read.html" %} +{% block subtitle %}{{ _('Activity Stream') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} + {% block primary_content_inner %}

    {{ _('Activity Stream') }}

    From 3d0361c74f2b79fd9dadac83a53ad280636386b4 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 5 Nov 2012 17:45:19 +0100 Subject: [PATCH 10/14] [#3005] Add num_followers to group_dict returned by group_read So that templates can access a group's number of followers without having to make another API call. --- ckan/logic/action/get.py | 4 ++++ ckan/public/base/javascript/modules/popover-context.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 16e0abfa79a..91d3bc41c5a 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -730,6 +730,10 @@ def group_show(context, data_dict): except AttributeError: schema = group_plugin.db_to_form_schema() + group_dict['num_followers'] = logic.get_action('group_follower_count')( + {'model': model, 'session': model.Session}, + {'id': group_dict['id']}) + if schema: group_dict, errors = _validate(group_dict, schema, context=context) return group_dict diff --git a/ckan/public/base/javascript/modules/popover-context.js b/ckan/public/base/javascript/modules/popover-context.js index e44fafbf6dc..b3fdb188656 100644 --- a/ckan/public/base/javascript/modules/popover-context.js +++ b/ckan/public/base/javascript/modules/popover-context.js @@ -191,7 +191,7 @@ this.ckan.module('popover-context', function($, _) { params.name = raw.name; params.description = raw.description; params.num_datasets = raw.packages.length; - //params.num_followers = raw.num_followers; + params.num_followers = raw.num_followers; } return params; }, From be2505cf67fce6f374f56de07126422d1cc816c6 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Mon, 5 Nov 2012 18:30:15 +0100 Subject: [PATCH 11/14] Fix a broken activity streams test --- ckan/tests/functional/test_activity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckan/tests/functional/test_activity.py b/ckan/tests/functional/test_activity.py index 56772ce3374..c881862326a 100644 --- a/ckan/tests/functional/test_activity.py +++ b/ckan/tests/functional/test_activity.py @@ -137,7 +137,7 @@ def test_user_activity(self): group = group_create(context, group) result = self.app.get(offset, status=200) stripped = self.strip_tags(result) - assert '%s created the group %s' % (user['fullname'], group['name']) \ + assert '%s created the group %s' % (user['fullname'], group['title']) \ in stripped, stripped # Update the group. @@ -145,7 +145,7 @@ def test_user_activity(self): group = group_update(context, group) result = self.app.get(offset, status=200) stripped = self.strip_tags(result) - assert '%s updated the group %s' % (user['fullname'], group['name']) \ + assert '%s updated the group %s' % (user['fullname'], group['title']) \ in stripped, stripped # Delete the group. @@ -153,7 +153,7 @@ def test_user_activity(self): group_update(context, group) result = self.app.get(offset, status=200) stripped = self.strip_tags(result) - assert '%s deleted the group %s' % (user['fullname'], group['name']) \ + assert '%s deleted the group %s' % (user['fullname'], group['title']) \ in stripped, stripped # Add a new tag to the package. From 5a66e8c018c6eae6e429de0eb30ef4e2d5a0106b Mon Sep 17 00:00:00 2001 From: John Martin Date: Mon, 5 Nov 2012 18:35:54 +0000 Subject: [PATCH 12/14] Added about page to groups and fixed the truncate error on group/read.html --- ckan/config/routing.py | 1 + ckan/controllers/group.py | 27 ++++++++++++------------ ckan/lib/activity_streams.py | 6 +++--- ckan/templates/group/about.html | 37 +++++++++++++++++++++++++++++++++ ckan/templates/group/read.html | 12 ++++++----- 5 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 ckan/templates/group/about.html diff --git a/ckan/config/routing.py b/ckan/config/routing.py index fc5a81584e5..44df60358c0 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -242,6 +242,7 @@ def make_map(): 'follow', 'unfollow', 'admins', + 'about', 'activity', ])) ) diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 2e76081764e..b08cb22daeb 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -551,21 +551,21 @@ def unfollow(self, id): h.redirect_to(controller='group', action='read', id=id) def followers(self, id=None): - context = {'model': model, 'session': model.Session, - 'user': c.user or c.author, 'for_view': True} - data_dict = {'id': id} - try: - c.group_dict = get_action('group_show')(context, data_dict) - c.followers = get_action('group_follower_list')(context, - {'id': c.group_dict['id']}) - except NotFound: - abort(404, _('Group not found')) - except NotAuthorized: - abort(401, _('Unauthorized to read group %s') % id) - + context = self._get_group_dict(id) + c.followers = get_action('group_follower_list')(context, + {'id': c.group_dict['id']}) return render('group/followers.html') def admins(self, id=None): + context = self._get_group_dict(id) + c.admins = self.authorizer.get_admins(context['group']) + return render('group/admins.html') + + def about(self, id=None): + self._get_group_dict(id) + return render('group/about.html') + + def _get_group_dict(self, id): context = {'model': model, 'session': model.Session, 'user': c.user or c.author, 'for_view': True} @@ -577,8 +577,7 @@ def admins(self, id=None): abort(404, _('Group not found')) except NotAuthorized: abort(401, _('Unauthorized to read group %s') % id) - - return render('group/admins.html') + return context def _render_edit_form(self, fs): # errors arrive in c.error and fs.errors diff --git a/ckan/lib/activity_streams.py b/ckan/lib/activity_streams.py index 9ac8eb9a887..457767e5934 100644 --- a/ckan/lib/activity_streams.py +++ b/ckan/lib/activity_streams.py @@ -161,16 +161,16 @@ def activity_stream_string_new_related_item(): # A dictionary mapping activity types to the icons associated to them activity_stream_string_icons = { 'added tag': 'tag', - 'changed group': 'users', + 'changed group': 'group', 'changed package': 'sitemap', 'changed package_extra': 'edit', 'changed resource': 'file', 'changed user': 'user', - 'deleted group': 'users', + 'deleted group': 'group', 'deleted package': 'sitemap', 'deleted package_extra': 'edit', 'deleted resource': 'file', - 'new group': 'users', + 'new group': 'group', 'new package': 'sitemap', 'new package_extra': 'edit', 'new resource': 'file', diff --git a/ckan/templates/group/about.html b/ckan/templates/group/about.html new file mode 100644 index 00000000000..985abe5fd1d --- /dev/null +++ b/ckan/templates/group/about.html @@ -0,0 +1,37 @@ +{% extends "group/read.html" %} + +{% block subtitle %}{{ _('About') }} - {{ c.group_dict.title or c.group_dict.name }}{% endblock %} + +{% block primary_content_inner %} +
    +

    {{ _('About') }}

    + + + + + + + + + + + + + + + + + +
    Name:{{ c.group_dict.title or c.group_dict.name }}
    Description: + {% if c.group_dict.description %} +

    {{ c.group_dict.description }}

    + {% else %} +

    {{ _('There is no description for this group') }}

    + {% endif %} +
    URL: + + {{ h.url_for(controller='group', action='read', id=c.group_dict.name, qualified=true) }} + +
    +
    +{% endblock %} diff --git a/ckan/templates/group/read.html b/ckan/templates/group/read.html index edc5625a442..5e36a3763b6 100644 --- a/ckan/templates/group/read.html +++ b/ckan/templates/group/read.html @@ -32,6 +32,9 @@ {% link_for _('Administrators'), controller='group', action='admins', id=c.group_dict.name, icon='cog' %} + + {% link_for _('About'), controller='group', action='about', id=c.group_dict.name, icon='info-sign' %} + {% endblock %} @@ -54,11 +57,10 @@

    {{ c.group_dict.display_name }}

    {% if c.group_dict.description %} - {% if truncate %} -

    {{ c.group_dict.description }}

    - {% else %} -

    {{ h.markdown_extract(c.group_dict.description, truncate) }}

    - {% endif %} +

    + {{ h.markdown_extract(c.group_dict.description, 180) }} + {% link_for _('read more'), controller='group', action='about', id=c.group_dict.name %} +

    {% else %}

    {{ _('There is no description for this group') }}

    {% endif %} From 115e70326ff2eef3f42b33c79b77ad55866ef5cf Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Tue, 6 Nov 2012 13:31:17 +0100 Subject: [PATCH 13/14] [#3005] Add back c.group_admins on group read teamplate It's used by the legacy templates to show a list of the group's admins in the sidebar. Legacy templates do not support the new group admins page. This fixes a frontend test that was failing while testing the legacy templates. --- ckan/controllers/group.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index b08cb22daeb..170565da371 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -131,6 +131,10 @@ def read(self, id): context['return_query'] = True + # c.group_admins is used by CKAN's legacy (Genshi) templates only, + # if we drop support for those then we can delete this line. + c.group_admins = self.authorizer.get_admins(c.group) + limit = 20 try: page = int(request.params.get('page', 1)) From 8cdd117bb1ba22e7e9fa962ed9aea8152edae424 Mon Sep 17 00:00:00 2001 From: John Martin Date: Wed, 14 Nov 2012 13:02:44 +0000 Subject: [PATCH 14/14] Couple of minor IE hack CSS tweaks to 2.0 theme --- ckan/public/base/less/iehacks.less | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ckan/public/base/less/iehacks.less b/ckan/public/base/less/iehacks.less index 1035ad803d6..ece1e36bd2d 100644 --- a/ckan/public/base/less/iehacks.less +++ b/ckan/public/base/less/iehacks.less @@ -48,6 +48,29 @@ } } +// Internet Explorer 7 + 8 +.ie7, +.ie8 { + .masthead { + nav ul li a.active { + position: relative; + top: -1px; + background-color: lighten(@mastheadBackgroundColorStart, 2); + border-top: 1px solid darken(@mastheadBackgroundColorStart, 3); + border-bottom: 1px solid lighten(@mastheadBackgroundColorStart, 5); + } + } +} + + +// Internet Explorer 8 +.ie8 { + .masthead .account a.image { + display: block; + width: 25px; + } +} + // Internet Explorer 7 .ie7 { @@ -133,6 +156,8 @@ // Header .masthead { + position: relative; + z-index: 1; .logo img, nav { .ie7-inline-block; @@ -153,6 +178,9 @@ .header-image { display: block; } + .account .dropdown-menu { + z-index: 10000; + } } // Footer