8000 Auto Paging for Users in Groups (#204) · gaoang2148/server-client-python@c6ab7da · GitHub
[go: up one dir, main page]

Skip to content

Commit c6ab7da

Browse files
authored
Auto Paging for Users in Groups (tableau#204)
This update has three main components: - Update the `Pager` to be able to iterate over any callable that returns (List[Model], PaginationItem). - Update the Groups Endpoint to call the new `Pager` and add a method that just gets users in a group - Update `populate_users` to create a closure wrapping Pager and request options and an endpoint and then bind that to the `GroupItem.users` attribute so that each call generates a new `Pager` Now, calling group.users will return a Pager that's always fresh. You can iterate it like a list, or convert it to a list if needed with `list(group.users)`
1 parent 9ac90e0 commit c6ab7da

File tree

6 files changed

+84
-54
lines changed

6 files changed

+84
-54
lines changed

tableauserverclient/models/group_item.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def users(self):
3333
if self._users is None:
3434
error = "Group must be populated with users first."
3535
raise UnpopulatedPropertyError(error)
36-
return self._users
36+
# Each call to `.users` should create a new pager, this just runs the callable
37+
return self._users()
3738

3839
def _set_users(self, users):
3940
self._users = users

tableauserverclient/server/endpoint/groups_endpoint.py

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,26 @@ def get(self, req_options=None):
2525
# Gets all users in a given group
2626
@api(version="2.0")
2727
def populate_users(self, group_item, req_options=None):
28+
from .. import Pager
29+
2830
if not group_item.id:
2931
error = "Group item missing ID. Group must be retrieved from server first."
3032
raise MissingRequiredFieldError(error)
33+
34+
# Define an inner function that we bind to the model_item's `.user` property.
35+
36+
def user_pager():
37+
return Pager(lambda options: self._get_users_for_group(group_item, options), req_options)
38+
39+
group_item._set_users(user_pager)
40+
41+
def _get_users_for_group(self, group_item, req_options=None):
3142
url = "{0}/{1}/users".format(self.baseurl, group_item.id)
3243
server_response = self.get_request(url, req_options)
33-
group_item._set_users(UserItem.from_response(server_response.content))
44+
user_item = UserItem.from_response(server_response.content)
3445
pagination_item = PaginationItem.from_response(server_response.content)
3546
logger.info('Populated users for group (ID: {0})'.format(group_item.id))
36-
return pagination_item
47+
return user_item, pagination_item
3748

3849
# Deletes 1 group by id
3950
@api(version="2.0")
@@ -56,32 +67,6 @@ def create(self, group_item):
5667
# Removes 1 user from 1 group
5768
@api(version="2.0")
5869
def remove_user(self, group_item, user_id):
59-
self._remove_user(group_item, user_id)
60-
try:
61-
users = group_item.users
62-
for user in users:
63-
if user.id == user_id:
64-
users.remove(user)
65-
break
66-
except UnpopulatedPropertyError:
67-
# If we aren't populated, do nothing to the user list
68-
pass
69-
logger.info('Removed user (id: {0}) from group (ID: {1})'.format(user_id, group_item.id))
70-
71-
# Adds 1 user to 1 group
72-
@api(version="2.0")
73-
def add_user(self, group_item, user_id):
74-
new_user = self._add_user(group_item, user_id)
75-
try:
76-
users = group_item.users
77-
users.append(new_user)
78-
group_item._set_users(users)
79-
except UnpopulatedPropertyError:
80-
# If we aren't populated, do nothing to the user list
81-
pass
82-
logger.info('Added user (id: {0}) to group (ID: {1})'.format(user_id, group_item.id))
83-
84-
def _remove_user(self, group_item, user_id):
8570
if not group_item.id:
8671
error = "Group item missing ID."
8772
raise MissingRequiredFieldError(error)
@@ -90,8 +75,11 @@ def _remove_user(self, group_item, user_id):
9075
raise ValueError(error)
9176
url = "{0}/{1}/users/{2}".format(self.baseurl, group_item.id, user_id)
9277
self.delete_request(url)
78+
logger.info('Removed user (id: {0}) from group (ID: {1})'.format(user_id, group_item.id))
9379

94-
def _add_user(self, group_item, user_id):
80+
# Adds 1 user to 1 group
81+
@api(version="2.0")
82+
def add_user(self, group_item, user_id):
9583
if not group_item.id:
9684
error = "Group item missing ID."
9785
raise MissingRequiredFieldError(error)
@@ -102,3 +90,4 @@ def _add_user(self, group_item, user_id):
10290
add_req = RequestFactory.Group.add_user_req(user_id)
10391
server_response = self.post_request(url, add_req)
10492
return UserItem.from_response(server_response.content).pop()
93+
logger.info('Added user (id: {0}) to group (ID: {1})'.format(user_id, group_item.id))

tableauserverclient/server/pager.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,25 @@
44

55
class Pager(object):
66
"""
7-
Generator that takes an endpoint with `.get` and lazily loads items from Server.
8-
Supports all `RequestOptions` including starting on any page.
7+
Generator that takes an endpoint (top level endpoints with `.get)` and lazily loads items from Server.
8+
Supports all `RequestOptions` including starting on any page. Also used by models to load sub-models
9+
(users in a group, views in a workbook, etc) by passing a different endpoint.
10+
11+
Will loop over anything that returns (List[ModelItem], PaginationItem).
912
"""
1013

1114
def __init__(self, endpoint, request_opts=None):
12-
self._endpoint = endpoint.get
15+
16+
if hasattr(endpoint, 'get'):
17+
# The simpliest case is to take an Endpoint and call its get
18+
self._endpoint = endpoint.get
19+
elif callable(endpoint):
20+
# but if they pass a callable then use that instead (used internally)
21+
self._endpoint = endpoint
22+
else:
23+
# Didn't get something we can page over
24+
raise ValueError("Pager needs a server endpoint to page through.")
25+
1326
self._options = request_opts
1427

1528
# If we have options we could be starting on any page, backfill the count
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.3.xsd">
3+
<pagination pageNumber="1" pageSize="100" totalAvailable="1" />
4+
<users>
5+
</users>
6+
</tsResponse>

test/assets/group_users_added.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse xmlns="http://tableau.com/api" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.3.xsd">
3+
<pagination pageNumber="1" pageSize="100" totalAvailable="1" />
4+
<users>
5+
<user id="5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" name="testuser" siteRole="ServerAdministrator" externalAuthUserId="" />
6+
</users>
7+
</tsResponse>

test/test_group.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99

1010
GET_XML = os.path.join(TEST_ASSET_DIR, 'group_get.xml')
1111
POPULATE_USERS = os.path.join(TEST_ASSET_DIR, 'group_populate_users.xml')
12+
POPULATE_USERS_EMPTY = os.path.join(TEST_ASSET_DIR, 'group_populate_users_empty.xml')
1213
ADD_USER = os.path.join(TEST_ASSET_DIR, 'group_add_user.xml')
14+
ADD_USER_POPULATE = os.path.join(TEST_ASSET_DIR, 'group_users_added.xml')
1315
CREATE_GROUP = os.path.join(TEST_ASSET_DIR, 'group_create.xml')
1416
CREATE_GROUP_ASYNC = os.path.join(TEST_ASSET_DIR, 'group_create_async.xml')
1517

@@ -52,17 +54,18 @@ def test_populate_users(self):
5254
with open(POPULATE_USERS, 'rb') as f:
5355
response_xml = f.read().decode('utf-8')
5456
with requests_mock.mock() as m:
55-
m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml)
57+
m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users?pageNumber=1&pageSize=100&sort=name:asc',
58+
text=response_xml, complete_qs=True)
5659
single_group = TSC.GroupItem(name='Test Group')
5760
single_group._id = 'e7833b48-c6f7-47b5-a2a7-36e7dd232758'
58-
pagination_item = self.server.groups.populate_users(single_group)
61+
self.server.groups.populate_users(single_group)
5962

60-
self.assertEqual(1, pagination_item.total_available)
61-
user = single_group.users.pop()
62-
self.assertEqual('dd2239f6-ddf1-4107-981a-4cf94e415794', user.id)
63-
self.assertEqual('alice', user.name)
64-
self.assertEqual('Publisher', user.site_role)
65-
self.assertEqual('2016-08-16T23:17:06Z', format_datetime(user.last_login))
63+
self.assertEqual(1, len(list(single_group.users)))
64+
user = list(single_group.users).pop()
65+
self.assertEqual('dd2239f6-ddf1-4107-981a-4cf94e415794', user.id)
66+
self.assertEqual('alice', user.name)
67+
self.assertEqual('Publisher', user.site_role)
68+
self.assertEqual('2016-08-16T23:17:06Z', format_datetime(user.last_login))
6669

6770
def test_delete(self):
6871
with requests_mock.mock() as m:
@@ -71,35 +74,46 @@ def test_delete(self):
7174

7275
def test_remove_user(self):
7376
with open(POPULATE_USERS, 'rb') as f:
74-
response_xml = f.read().decode('utf-8')
77+
response_xml_populate = f.read().decode('utf-8')
78+
79+
with open(POPULATE_USERS_EMPTY, 'rb') as f:
80+
response_xml_empty = f.read().decode('utf-8')
81+
7582
with requests_mock.mock() as m:
7683
url = self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users' \
7784
'/dd2239f6-ddf1-4107-981a-4cf94e415794'
85+
7886
m.delete(url, status_code=204)
79-
m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml)
87+
# We register the get endpoint twice. The first time we have 1 user, the second we have 'removed' them.
88+
m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml_populate)
89+
8090
single_group = TSC.GroupItem('test')
8191
single_group._id = 'e7833b48-c6f7-47b5-a2a7-36e7dd232758'
8292
self.server.groups.populate_users(single_group)
83-
self.assertEqual(1, len(single_group.users))
93+
self.assertEqual(1, len(list(single_group.users)))
8494
self.server.groups.remove_user(single_group, 'dd2239f6-ddf1-4107-981a-4cf94e415794')
8595

86-
self.assertEqual(0, len(single_group.users))
96+
m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml_empty)
97+
self.assertEqual(0, len(list(single_group.users)))
8798

8899
def test_add_user(self):
89100
with open(ADD_USER, 'rb') as f:
90-
response_xml = f.read().decode('utf-8')
101+
response_xml_add = f.read().decode('utf-8')
102+
with open(ADD_USER_POPULATE, 'rb') as f:
103+
response_xml_populate = f.read().decode('utf-8')
91104
with requests_mock.mock() as m:
92-
m.post(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml)
105+
m.post(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml_add)
106+
m.get(self.baseurl + '/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users', text=response_xml_populate)
93107
single_group = TSC.GroupItem('test')
94108
single_group._id = 'e7833b48-c6f7-47b5-a2a7-36e7dd232758'
95-
single_group._users = []
96-
self.server.groups.add_user(single_group, '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
97109

98-
self.assertEqual(1, len(single_group.users))
99-
user = single_group.users.pop()
100-
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', user.id)
101-
self.assertEqual('testuser', user.name)
102-
self.assertEqual('ServerAdministrator', user.site_role)
110+
self.server.groups.add_user(single_group, '5de011f8-5aa9-4d5b-b991-f462c8dd6bb7')
111+
self.server.groups.populate_users(single_group)
112+
self.assertEqual(1, len(list(single_group.users)))
113+
user = list(single_group.users).pop()
114+
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', user.id)
115+
self.assertEqual('testuser', user.name)
116+
self.assertEqual('ServerAdministrator', user.site_role)
103117

104118
def test_add_user_before_populating(self):
105119
with open(GET_XML, 'rb') as f:

0 commit comments

Comments
 (0)
0