Repository: ckan/ckan

Issue:
Update search index after membership changes



Discussion:

Resolving patch:
diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py
--- a/ckan/logic/action/get.py
+++ b/ckan/logic/action/get.py
@@ -577,15 +577,38 @@ def group_list_authz(context, data_dict):
 
 
 def organization_list_for_user(context, data_dict):
-    '''Return the list of organizations that the user is a member of.
+    '''Return the organizations that the user has a given permission for.
+
+    By default this returns the list of organizations that the currently
+    authorized user can edit, i.e. the list of organizations that the user is an
+    admin of.
+
+    Specifically it returns the list of organizations that the currently
+    authorized user has a given permission (for example: "edit_group") against.
+
+    When a user becomes a member of an organization in CKAN they're given a
+    "capacity" (sometimes called a "role"), for example "member", "editor" or
+    "admin".
+
+    Each of these roles has certain permissions associated with it. For example
+    the admin role has the "admin" permission (which means they have permission
+    to do anything). The editor role has permissions like "create_dataset",
+    "update_dataset" and "delete_dataset".  The member role has the "read"
+    permission.
+
+    This function returns the list of organizations that the authorized user has
+    a given permission for. For example the list of organizations that the user
+    is an admin of, or the list of organizations that the user can create
+    datasets in.
 
     :param permission: the permission the user has against the
-        returned organizations (optional, default: ``edit_group``)
+        returned organizations, for example ``"read"`` or ``"create_dataset"``
+        (optional, default: ``"edit_group"``)
     :type permission: string
 
-    :returns: list of dictized organizations that the user is
-        authorized to edit
+    :returns: list of organizations that the user has the given permission for
     :rtype: list of dicts
+
     '''
     model = context['model']
     user = context['user']
@@ -613,7 +636,8 @@ def organization_list_for_user(context, data_dict):
         q = model.Session.query(model.Member) \
             .filter(model.Member.table_name == 'user') \
             .filter(model.Member.capacity.in_(roles)) \
-            .filter(model.Member.table_id == user_id)
+            .filter(model.Member.table_id == user_id) \
+            .filter(model.Member.state == 'active')
 
         group_ids = []
         for row in q.all():


Test patch:
diff --git a/ckan/new_tests/logic/action/test_get.py b/ckan/new_tests/logic/action/test_get.py
--- a/ckan/new_tests/logic/action/test_get.py
+++ b/ckan/new_tests/logic/action/test_get.py
@@ -459,3 +459,302 @@ def test_package_search_facet_field_is_json(self):
         nose.tools.assert_raises(
             logic.ValidationError, helpers.call_action, 'package_search',
             **kwargs)
+
+
+class TestOrganizationListForUser(object):
+    '''Functional tests for the organization_list_for_user() action function.'''
+
+    def setup(self):
+        helpers.reset_db()
+        search.clear()
+
+    def test_when_user_is_not_a_member_of_any_organizations(self):
+        """
+
+        When the user isn't a member of any organizations (in any capacity)
+        organization_list_for_user() should return an empty list.
+
+        """
+        user = factories.User()
+        context = {'user': user['name']}
+
+        # Create an organization so we can test that it does not get returned.
+        factories.Organization()
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            context=context)
+
+        assert organizations == []
+
+    def test_when_user_is_an_admin_of_one_organization(self):
+        """
+
+        When the user is an admin of one organization
+        organization_list_for_user() should return a list of just that one
+        organization.
+
+        """
+        user = factories.User()
+        context = {'user': user['name']}
+        organization = factories.Organization()
+
+        # Create a second organization just so we can test that it does not get
+        # returned.
+        factories.Organization()
+
+        helpers.call_action('member_create', id=organization['id'],
+                            object=user['id'], object_type='user',
+                            capacity='admin')
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            context=context)
+
+        assert len(organizations) == 1
+        assert organizations[0]['id'] == organization['id']
+
+    def test_when_user_is_an_admin_of_three_organizations(self):
+        """
+
+        When the user is an admin of three organizations
+        organization_list_for_user() should return a list of all three
+        organizations.
+
+        """
+        user = factories.User()
+        context = {'user': user['name']}
+        organization_1 = factories.Organization()
+        organization_2 = factories.Organization()
+        organization_3 = factories.Organization()
+
+        # Create a second organization just so we can test that it does not get
+        # returned.
+        factories.Organization()
+
+        # Make the user an admin of all three organizations:
+        for organization in (organization_1, organization_2, organization_3):
+            helpers.call_action('member_create', id=organization['id'],
+                                object=user['id'], object_type='user',
+                                capacity='admin')
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            context=context)
+
+        assert len(organizations) == 3
+        ids = [organization['id'] for organization in organizations]
+        for organization in (organization_1, organization_2, organization_3):
+            assert organization['id'] in ids
+
+    def test_does_not_return_members(self):
+        """
+
+        By default organization_list_for_user() should not return organizations
+        that the user is just a member (not an admin) of.
+
+        """
+        user = factories.User()
+        context = {'user': user['name']}
+        organization = factories.Organization()
+
+        helpers.call_action('member_create', id=organization['id'],
+                            object=user['id'], object_type='user',
+                            capacity='member')
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            context=context)
+
+        assert organizations == []
+
+    def test_does_not_return_editors(self):
+        """
+
+        By default organization_list_for_user() should not return organizations
+        that the user is just an editor (not an admin) of.
+
+        """
+        user = factories.User()
+        context = {'user': user['name']}
+        organization = factories.Organization()
+
+        helpers.call_action('member_create', id=organization['id'],
+                            object=user['id'], object_type='user',
+                            capacity='editor')
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            context=context)
+
+        assert organizations == []
+
+    def test_editor_permission(self):
+        """
+
+        organization_list_for_user() should return organizations that the user
+        is an editor of if passed a permission that belongs to the editor role.
+
+        """
+        user = factories.User()
+        context = {'user': user['name']}
+        organization = factories.Organization()
+
+        helpers.call_action('member_create', id=organization['id'],
+                            object=user['id'], object_type='user',
+                            capacity='editor')
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            permission='create_dataset',
+                                            context=context)
+
+        assert [org['id'] for org in organizations] == [organization['id']]
+
+    def test_member_permission(self):
+        """
+
+        organization_list_for_user() should return organizations that the user
+        is a member of if passed a permission that belongs to the member role.
+
+        """
+        user = factories.User()
+        context = {'user': user['name']}
+        organization = factories.Organization()
+
+        helpers.call_action('member_create', id=organization['id'],
+                            object=user['id'], object_type='user',
+                            capacity='member')
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            permission='read',
+                                            context=context)
+
+        assert [org['id'] for org in organizations] == [organization['id']]
+
+    def test_invalid_permission(self):
+        '''
+
+        organization_list_for_user() should return an empty list if passed a
+        non-existent or invalid permission.
+
+        Note that we test this with a user who is an editor of one organization.
+        If the user was an admin of the organization then it would return that
+        organization - admins have all permissions, including permissions that
+        don't exist.
+
+        '''
+        user = factories.User()
+        context = {'user': user['name']}
+        organization = factories.Organization()
+        factories.Organization()
+        helpers.call_action('member_create', id=organization['id'],
+                            object=user['id'], object_type='user',
+                            capacity='editor')
+
+        for permission in ('', ' ', 'foo', 27.3, 5, True, False, None):
+            organizations = helpers.call_action('organization_list_for_user',
+                                                permission=permission,
+                                                context=context)
+
+        assert organizations == []
+
+    def test_that_it_does_not_return_groups(self):
+        """
+
+        organization_list_for_user() should not return groups that the user is
+        a member, editor or admin of.
+
+        """
+        user = factories.User()
+        context = {'user': user['name']}
+        group_1 = factories.Group()
+        group_2 = factories.Group()
+        group_3 = factories.Group()
+        helpers.call_action('member_create', id=group_1['id'],
+                            object=user['id'], object_type='user',
+                            capacity='member')
+        helpers.call_action('member_create', id=group_2['id'],
+                            object=user['id'], object_type='user',
+                            capacity='editor')
+        helpers.call_action('member_create', id=group_3['id'],
+                            object=user['id'], object_type='user',
+                            capacity='admin')
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            context=context)
+
+        assert organizations == []
+
+    def test_that_it_does_not_return_previous_memberships(self):
+        """
+
+        organization_list_for_user() should return organizations that the user
+        was previously an admin of.
+
+        """
+        user = factories.User()
+        context = {'user': user['name']}
+        organization = factories.Organization()
+
+        # Make the user an admin of the organization.
+        helpers.call_action('member_create', id=organization['id'],
+                            object=user['id'], object_type='user',
+                            capacity='admin')
+
+        # Remove the user from the organization.
+        helpers.call_action('member_delete', id=organization['id'],
+                            object=user['id'], object_type='user')
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            context=context)
+
+        assert organizations == []
+
+    def test_when_user_is_sysadmin(self):
+        """
+
+        When the user is a sysadmin organization_list_for_user() should just
+        return all organizations, even if the user is not a member of them.
+
+        """
+        user = factories.Sysadmin()
+        context = {'user': user['name']}
+        organization = factories.Organization()
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            context=context)
+
+        assert [org['id'] for org in organizations] == [organization['id']]
+
+    def test_that_it_does_not_return_deleted_organizations(self):
+        """
+
+        organization_list_for_user() should not return deleted organizations
+        that the user was an admin of.
+
+        """
+        user = factories.User()
+        context = {'user': user['name']}
+        organization = factories.Organization()
+
+        # Make the user an admin of the organization.
+        helpers.call_action('member_create', id=organization['id'],
+                            object=user['id'], object_type='user',
+                            capacity='admin')
+
+        # Delete the organization.
+        helpers.call_action('organization_delete', id=organization['id'])
+
+        organizations = helpers.call_action('organization_list_for_user',
+                                            context=context)
+
+        assert organizations == []
+
+    def test_with_no_authorized_user(self):
+        """
+
+        organization_list_for_user() should return an empty list if there's no
+        authorized user. Users who aren't logged-in don't have any permissions.
+
+        """
+        # Create an organization so we can test that it doesn't get returned.
+        organization = factories.Organization()
+
+        organizations = helpers.call_action('organization_list_for_user')
+
+        assert organizations == []