diff --git a/tableauserverclient/models/group_item.py b/tableauserverclient/models/group_item.py
index c0014eac0..ab627b569 100644
--- a/tableauserverclient/models/group_item.py
+++ b/tableauserverclient/models/group_item.py
@@ -33,7 +33,8 @@ def users(self):
if self._users is None:
error = "Group must be populated with users first."
raise UnpopulatedPropertyError(error)
- return self._users
+ # Each call to `.users` should create a new pager, this just runs the callable
+ return self._users()
def _set_users(self, users):
self._users = users
diff --git a/tableauserverclient/server/endpoint/groups_endpoint.py b/tableauserverclient/server/endpoint/groups_endpoint.py
index 243aa54c9..7c1a25c64 100644
--- a/tableauserverclient/server/endpoint/groups_endpoint.py
+++ b/tableauserverclient/server/endpoint/groups_endpoint.py
@@ -25,15 +25,26 @@ def get(self, req_options=None):
# Gets all users in a given group
@api(version="2.0")
def populate_users(self, group_item, req_options=None):
+ from .. import Pager
+
if not group_item.id:
error = "Group item missing ID. Group must be retrieved from server first."
raise MissingRequiredFieldError(error)
+
+ # Define an inner function that we bind to the model_item's `.user` property.
+
+ def user_pager():
+ return Pager(lambda options: self._get_users_for_group(group_item, options), req_options)
+
+ group_item._set_users(user_pager)
+
+ def _get_users_for_group(self, group_item, req_options=None):
url = "{0}/{1}/users".format(self.baseurl, group_item.id)
server_response = self.get_request(url, req_options)
- group_item._set_users(UserItem.from_response(server_response.content))
+ user_item = UserItem.from_response(server_response.content)
pagination_item = PaginationItem.from_response(server_response.content)
logger.info('Populated users for group (ID: {0})'.format(group_item.id))
- return pagination_item
+ return user_item, pagination_item
# Deletes 1 group by id
@api(version="2.0")
@@ -56,32 +67,6 @@ def create(self, group_item):
# Removes 1 user from 1 group
@api(version="2.0")
def remove_user(self, group_item, user_id):
- self._remove_user(group_item, user_id)
- try:
- users = group_item.users
- for user in users:
- if user.id == user_id:
- users.remove(user)
- break
- except UnpopulatedPropertyError:
- # If we aren't populated, do nothing to the user list
- pass
- logger.info('Removed user (id: {0}) from group (ID: {1})'.format(user_id, group_item.id))
-
- # Adds 1 user to 1 group
- @api(version="2.0")
- def add_user(self, group_item, user_id):
- new_user = self._add_user(group_item, user_id)
- try:
- users = group_item.users
- users.append(new_user)
- group_item._set_users(users)
- except UnpopulatedPropertyError:
- # If we aren't populated, do nothing to the user list
- pass
- logger.info('Added user (id: {0}) to group (ID: {1})'.format(user_id, group_item.id))
-
- def _remove_user(self, group_item, user_id):
if not group_item.id:
error = "Group item missing ID."
raise MissingRequiredFieldError(error)
@@ -90,8 +75,11 @@ def _remove_user(self, group_item, user_id):
raise ValueError(error)
url = "{0}/{1}/users/{2}".format(self.baseurl, group_item.id, user_id)
self.delete_request(url)
+ logger.info('Removed user (id: {0}) from group (ID: {1})'.format(user_id, group_item.id))
- def _add_user(self, group_item, user_id):
+ # Adds 1 user to 1 group
+ @api(version="2.0")
+ def add_user(self, group_item, user_id):
if not group_item.id:
error = "Group item missing ID."
raise MissingRequiredFieldError(error)
@@ -102,3 +90,4 @@ def _add_user(self, group_item, user_id):
add_req = RequestFactory.Group.add_user_req(user_id)
server_response = self.post_request(url, add_req)
return UserItem.from_response(server_response.content).pop()
+ logger.info('Added user (id: {0}) to group (ID: {1})'.format(user_id, group_item.id))
diff --git a/tableauserverclient/server/pager.py b/tableauserverclient/server/pager.py
index 1a6bfe17c..8d988f483 100644
--- a/tableauserverclient/server/pager.py
+++ b/tableauserverclient/server/pager.py
@@ -4,12 +4,25 @@
class Pager(object):
"""
- Generator that takes an endpoint with `.get` and lazily loads items from Server.
- Supports all `RequestOptions` including starting on any page.
+ Generator that takes an endpoint (top level endpoints with `.get)` and lazily loads items from Server.
+ Supports all `RequestOptions` including starting on any page. Also used by models to load sub-models
+ (users in a group, views in a workbook, etc) by passing a different endpoint.
+
+ Will loop over anything that returns (List[ModelItem], PaginationItem).
"""
def __init__(self, endpoint, request_opts=None):
- self._endpoint = endpoint.get
+
+ if hasattr(endpoint, 'get'):
+ # The simpliest case is to take an Endpoint and call its get
+ self._endpoint = endpoint.get
+ elif callable(endpoint):
+ # but if they pass a callable then use that instead (used internally)
+ self._endpoint = endpoint
+ else:
+ # Didn't get something we can page over
+ raise ValueError("Pager needs a server endpoint to page through.")
+
self._options = request_opts
# If we have options we could be starting on any page, backfill the count
diff --git a/test/assets/group_populate_users_empty.xml b/test/assets/group_populate_users_empty.xml
new file mode 100644
index 000000000..5d921df03
--- /dev/null
+++ b/test/assets/group_populate_users_empty.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/assets/group_users_added.xml b/test/assets/group_users_added.xml
new file mode 100644
index 000000000..41768d1c9
--- /dev/null
+++ b/test/assets/group_users_added.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/test_group.py b/test/test_group.py
index 20c45455d..4886634fc 100644
--- a/test/test_group.py
+++ b/test/test_group.py
@@ -9,7 +9,9 @@
GET_XML = os.path.join(TEST_ASSET_DIR, 'group_get.xml')
POPULATE_USERS = os.path.join(TEST_ASSET_DIR, 'group_populate_users.xml')
+POPULATE_USERS_EMPTY = os.path.join(TEST_ASSET_DIR, 'group_populate_users_empty.xml')
ADD_USER = os.path.join(TEST_ASSET_DIR, 'group_add_user.xml')
+ADD_USER_POPULATE = os.path.join(TEST_ASSET_DIR, 'group_users_added.xml')
CREATE_GROUP = os.path.join(TEST_ASSET_DIR, 'group_create.xml')
CREATE_GROUP_ASYNC = os.path.join(TEST_ASSET_DIR, 'group_create_async.xml')
@@ -52,17 +54,18 @@ def test_populate_users(self):
with open(POPULATE_USERS, 'rb') as f:
response_xml = f.read().decode('utf-8')
with requests_mock.mock() as m:
- m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml)
+ m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users?pageNumber=1&pageSize=100&sort=name:asc',
+ text=response_xml, complete_qs=True)
single_group = TSC.GroupItem(name='Test Group')
single_group._id = 'e7833b48-c6f7-47b5-a2a7-36e7dd232758'
- pagination_item = self.server.groups.populate_users(single_group)
+ self.server.groups.populate_users(single_group)
- self.assertEqual(1, pagination_item.total_available)
- user = single_group.users.pop()
- self.assertEqual('dd2239f6-ddf1-4107-981a-4cf94e415794', user.id)
- self.assertEqual('alice', user.name)
- self.assertEqual('Publisher', user.site_role)
- self.assertEqual('2016-08-16T23:17:06Z', format_datetime(user.last_login))
+ self.assertEqual(1, len(list(single_group.users)))
+ user = list(single_group.users).pop()
+ self.assertEqual('dd2239f6-ddf1-4107-981a-4cf94e415794', user.id)
+ self.assertEqual('alice', user.name)
+ self.assertEqual('Publisher', user.site_role)
+ self.assertEqual('2016-08-16T23:17:06Z', format_datetime(user.last_login))
def test_delete(self):
with requests_mock.mock() as m:
@@ -71,35 +74,46 @@ def test_delete(self):
def test_remove_user(self):
with open(POPULATE_USERS, 'rb') as f:
- response_xml = f.read().decode('utf-8')
+ response_xml_populate = f.read().decode('utf-8')
+
+ with open(POPULATE_USERS_EMPTY, 'rb') as f:
+ response_xml_empty = f.read().decode('utf-8')
+
with requests_mock.mock() as m:
url = self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users' \
'/dd2239f6-ddf1-4107-981a-4cf94e415794'
+
m.delete(url, status_code=204)
- m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml)
+ # We register the get endpoint twice. The first time we have 1 user, the second we have 'removed' them.
+ m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml_populate)
+
single_group = TSC.GroupItem('test')
single_group._id = 'e7833b48-c6f7-47b5-a2a7-36e7dd232758'
self.server.groups.populate_users(single_group)
- self.assertEqual(1, len(single_group.users))
+ self.assertEqual(1, len(list(single_group.users)))
self.server.groups.remove_user(single_group, 'dd2239f6-ddf1-4107-981a-4cf94e415794')
- self.assertEqual(0, len(single_group.users))
+ m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml_empty)
+ self.assertEqual(0, len(list(single_group.users)))
def test_add_user(self):
with open(ADD_USER, 'rb') as f:
- response_xml = f.read().decode('utf-8')
+ response_xml_add = f.read().decode('utf-8')
+ with open(ADD_USER_POPULATE, 'rb') as f:
+ response_xml_populate = f.read().decode('utf-8')
with requests_mock.mock() as m:
- m.post(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml)
+ m.post(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml_add)
+ m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml_populate)
single_group = TSC.GroupItem('test')
single_group._id = 'e7833b48-c6f7-47b5-a2a7-36e7dd232758'
- single_group._users = []
- self.server.groups.add_user(single_group, '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
- self.assertEqual(1, len(single_group.users))
- user = single_group.users.pop()
- self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', user.id)
- self.assertEqual('testuser', user.name)
- self.assertEqual('ServerAdministrator', user.site_role)
+ self.server.groups.add_user(single_group, '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
+ self.server.groups.populate_users(single_group)
+ self.assertEqual(1, len(list(single_group.users)))
+ user = list(single_group.users).pop()
+ self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', user.id)
+ self.assertEqual('testuser', user.name)
+ self.assertEqual('ServerAdministrator', user.site_role)
def test_add_user_before_populating(self):
with open(GET_XML, 'rb') as f: