8000 Merge pull request #674 from tableau/bulk-users · tableau/server-client-python@c5a5d75 · GitHub
[go: up one dir, main page]

Skip to content

Commit c5a5d75

Browse files
authored
Merge pull request #674 from tableau/bulk-users
AD groups
2 parents b355c8f + a90fcf2 commit c5a5d75

File tree

8 files changed

+142
-17
lines changed

8 files changed

+142
-17
lines changed

tableauserverclient/models/group_item.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import xml.etree.ElementTree as ET
22
from .exceptions import UnpopulatedPropertyError
3-
from .property_decorators import property_not_empty
3+
from .property_decorators import property_not_empty, property_is_enum
44
from .reference_item import ResourceReference
5+
from .user_item import UserItem
56

67

78
class GroupItem(object):
@@ -13,11 +14,17 @@ def __init__(self, name=None):
1314
self._id = None
1415
self._users = None
1516
self.name = name
17+
self._license_mode = None
18+
self._minimum_site_role = None
1619

1720
@property
1821
def domain_name(self):
1922
return self._domain_name
2023

24+
@domain_name.setter
25+
def domain_name(self, value):
26+
self._domain_name = value
27+
2128
@property
2229
def id(self):
2330
return self._id
@@ -31,6 +38,24 @@ def name(self):
3138
def name(self, value):
3239
self._name = value
3340

41+
@property
42+
def license_mode(self):
43+
return self._license_mode
44+
45+
@license_mode.setter
46+
def license_mode(self, value):
47+
# valid values = onSync, onLogin
48+
self._license_mode = value
49+
50+
@property
51+
def minimum_site_role(self):
52+
return self._minimum_site_role
53+
54+
@minimum_site_role.setter
55+
@property_is_enum(UserItem.Roles)
56+
def minimum_site_role(self, value):
57+
self._minimum_site_role = value
58+
3459
@property
3560
def users(self):
3661
if self._users is None:
@@ -54,10 +79,18 @@ def from_response(cls, resp, ns):
5479
name = group_xml.get('name', None)
5580
group_item = cls(name)
5681
group_item._id = group_xml.get('id', None)
82+
# AD groups have an extra element under this
83+
import_elem = group_xml.find('.//t:import', namespaces=ns)
84+
if (import_elem is not None):
85+
group_item.domain_name = import_elem.get('domainName')
86+
group_item.license_mode = import_elem.get('grantLicenseMode')
87+
group_item.minimum_site_role = import_elem.get('siteRole')
88+
else:
89+
# local group, we will just have two extra attributes here
90+
group_item.domain_name = 'local'
91+
group_item.license_mode = group_xml.get('grantLicenseMode')
92+
group_item.minimum_site_role = group_xml.get('siteRole')
5793

58-
domain_elem = group_xml.find('.//t:domain', namespaces=ns)
59-
if domain_elem is not None:
60-
group_item._domain_name = domain_elem.get('name', None)
6194
all_group_items.append(group_item)
6295
return all_group_items
6396

tableauserverclient/models/job_item.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
class JobItem(object):
66
def __init__(self, id_, job_type, progress, created_at, started_at=None,
7-
completed_at=None, finish_code=0, notes=None):
7+
completed_at=None, finish_code=0, notes=None, mode=None):
88
self._id = id_
99
self._type = job_type
1010
self._progress = progress
@@ -13,6 +13,7 @@ def __init__(self, id_, job_type, progress, created_at, started_at=None,
1313
self._completed_at = completed_at
1414
self._finish_code = finish_code
1515
self._notes = notes or []
16+
self._mode = mode
1617

1718
@property
1819
def id(self):
@@ -46,6 +47,15 @@ def finish_code(self):
4647
def notes(self):
4748
return self._notes
4849

50+
@property
51+
def mode(self):
52+
return self._mode
53+
54+
@mode.setter
55+
def mode(self, value):
56+
# check for valid data here
57+
self._mode = value
58+
4959
def __repr__(self):
5060
return "<Job#{_id} {_type} created_at({_created_at}) started_at({_started_at}) completed_at({_completed_at})" \
5161
" progress ({_progress}) finish_code({_finish_code})>".format(**self.__dict__)
@@ -71,7 +81,8 @@ def _parse_element(cls, element, ns):
7181
finish_code = element.get('finishCode', -1)
7282
notes = [note.text for note in
7383
element.findall('.//t:notes', namespaces=ns)] or None
74-
return cls(id_, type_, progress, created_at, started_at, completed_at, finish_code, notes)
84+
mode = element.get('mode', None)
85+
return cls(id_, type_, progress, created_at, started_at, completed_at, finish_code, notes, mode)
7586

7687

7788
class BackgroundJobItem(object):

tableauserverclient/server/endpoint/groups_endpoint.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .endpoint import Endpoint, api
22
from .exceptions import MissingRequiredFieldError
3-
from .. import RequestFactory, GroupItem, UserItem, PaginationItem
3+
from .. import RequestFactory, GroupItem, UserItem, PaginationItem, JobItem
44
from ..pager import Pager
55

66
import logging
@@ -58,25 +58,39 @@ def delete(self, group_id):
5858
logger.info('Deleted single group (ID: {0})'.format(group_id))
5959

6060
@api(version="2.0")
61-
def update(self, group_item, default_site_role=UNLICENSED_USER):
61+
def update(self, group_item, default_site_role=UNLICENSED_USER, as_job=False):
6262
if not group_item.id:
6363
error = "Group item missing ID."
6464
raise MissingRequiredFieldError(error)
6565
url = "{0}/{1}".format(self.baseurl, group_item.id)
6666
update_req = RequestFactory.Group.update_req(group_item, default_site_role)
6767
server_response = self.put_request(url, update_req)
6868
logger.info('Updated group item (ID: {0})'.format(group_item.id))
69-
updated_group = GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]
70-
return updated_group
69+
if (as_job):
70+
return JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
71+
else:
72+
return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]
7173

7274
# Create a 'local' Tableau group
7375
@api(version="2.0")
7476
def create(self, group_item):
7577
url = self.baseurl
76-
create_req = RequestFactory.Group.create_req(group_item)
78+
create_req = RequestFactory.Group.create_local_req(group_item)
7779
server_response = self.post_request(url, create_req)
7880
return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]
7981

82+
# Create a group based on Active Directory
83+
@api(version="2.0")
84+
def create_AD_group(self, group_item, asJob=False):
85+
asJobparameter = "?asJob=true" if asJob else ""
86+
url = self.baseurl + asJobparameter
87+
create_req = RequestFactory.Group.create_ad_req(group_item)
88+
server_response = self.post_request(url, create_req)
89+
if (asJob):
90+
return JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
91+
else:
92+
return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]
93+
8094
# Removes 1 user from 1 group
8195
@api(version="2.0")
8296
def remove_user(self, group_item, user_id):
@@ -102,5 +116,6 @@ def add_user(self, group_item, user_id):
102116
url = "{0}/{1}/users".format(self.baseurl, group_item.id)
103117
add_req = RequestFactory.Group.add_user_req(user_id)
104118
server_response = self.post_request(url, add_req)
105-
return UserItem.from_response(server_response.content, self.parent_srv.namespace).pop()
119+
user = UserItem.from_response(server_response.content, self.parent_srv.namespace).pop()
106120
logger.info('Added user (id: {0}) to group (ID: {1})'.format(user_id, group_item.id))
121+
return user

tableauserverclient/server/request_factory.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,21 +238,46 @@ def add_user_req(self, user_id):
238238
user_element.attrib['id'] = user_id
239239
return ET.tostring(xml_request)
240240

241-
def create_req(self, group_item):
241+
def create_local_req(self, group_item):
242242
xml_request = ET.Element('tsRequest')
243243
group_element = ET.SubElement(xml_request, 'group')
244244
group_element.attrib['name'] = group_item.name
245+
if group_item.license_mode is not None:
246+
group_element.attrib['grantLicenseMode'] = group_item.license_mode
247+
if group_item.minimum_site_role is not None:
248+
group_element.attrib['SiteRole'] = group_item.minimum_site_role
245249
return ET.tostring(xml_request)
246250

247-
def update_req(self, group_item, default_site_role):
251+
def create_ad_req(self, group_item):
252+
xml_request = ET.Element('tsRequest')
253+
group_element = ET.SubElement(xml_request, 'group')
254+
group_element.attrib['name'] = group_item.name
255+
import_element = ET.SubElement(group_element, 'import')
256+
import_element.attrib['source'] = "ActiveDirectory"
257+
if group_item.domain_name is None:
258+
error = "Group Domain undefined."
259+
raise ValueError(error)
260+
261+
import_element.attrib['domainName'] = group_item.domain_name
262+
if group_item.license_mode is not None:
263+
import_element.attrib['grantLicenseMode'] = group_item.license
264+
if group_item.minimum_site_role is not None:
265+
import_element.attrib['SiteRole'] = group_item.minimum_site_role
266+
return ET.tostring(xml_request)
267+
268+
def update_req(self, group_item, default_site_role=None):
269+
if default_site_role is not None:
270+
group_item.minimum_site_role = default_site_role
248271
xml_request = ET.Element('tsRequest')
249272
group_element = ET.SubElement(xml_request, 'group')
250273
group_element.attrib['name'] = group_item.name
251274
if group_item.domain_name != 'local':
252275
project_element = ET.SubElement(group_element, 'import')
253276
project_element.attrib['source'] = "ActiveDirectory"
254277
project_element.attrib['domainName'] = group_item.domain_name
255-
project_element.attrib['siteRole'] = default_site_role
278+
project_element.attrib['siteRole'] = group_item.minimum_site_role
279+
project_element.attrib['grantLicenseMode'] = group_item.license_mode
280+
256281
return ET.tostring(xml_request)
257282

258283

test/assets/group_create.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
<tsResponse xmlns="http://tableau.com/api"
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44
xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.3.xsd">
5-
<group id="3e4a9ea0-a07a-4fe6-b50f-c345c8c81034" name="試供品" />
5+
<group id="3e4a9ea0-a07a-4fe6-b50f-c345c8c81034" name="試供品"
6+
siteRole="ExplorerCanPublish"
7+
grantLicenseMode='onLogin'/>
68
</tsResponse>

test/assets/group_create_ad.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<tsResponse xmlns="http://tableau.com/api"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.3.xsd">
5+
<group id="3e4a9ea0-a07a-4fe6-b50f-c345c8c81034" name="試供品" >
6+
<import domainName='active-directory-domain-name'
7+
siteRole='Creator'
8+
grantLicenseMode='onLogin' />
9+
</group>
10+
</tsResponse>

test/assets/group_update.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
<tsResponse xmlns="http://tableau.com/api"
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44
xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.3.xsd">
5-
<group id="ef8b19c0-43b6-11e6-af50-63f5805dbe3c" name="Group updated name" />
5+
<group id="ef8b19c0-43b6-11e6-af50-63f5805dbe3c" name="Group updated name"
6+
siteRole="ExplorerCanPublish"
7+
grantLicenseMode='onLogin'/>/>
68
</tsResponse>

test/test_group.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
ADD_USER = os.path.join(TEST_ASSET_DIR, 'group_add_user.xml')
1414
ADD_USER_POPULATE = os.path.join(TEST_ASSET_DIR, 'group_users_added.xml')
1515
CREATE_GROUP = os.path.join(TEST_ASSET_DIR, 'group_create.xml')
16+
CREATE_GROUP_AD = os.path.join(TEST_ASSET_DIR, 'group_create_ad.xml')
1617
CREATE_GROUP_ASYNC = os.path.join(TEST_ASSET_DIR, 'group_create_async.xml')
1718
UPDATE_XML = os.path.join(TEST_ASSET_DIR, 'group_update.xml')
1819

@@ -185,6 +186,30 @@ def test_create_group(self):
185186
self.assertEqual(group.name, u'試供品')
186187
self.assertEqual(group.id, '3e4a9ea0-a07a-4fe6-b50f-c345c8c81034')
187188

189+
def test_create_ad_group(self):
190+
with open(CREATE_GROUP_AD, 'rb') as f:
191+
response_xml = f.read().decode('utf-8')
192+
with requests_mock.mock() as m:
193+
m.post(self.baseurl, text=response_xml)
194+
group_to_create = TSC.GroupItem(u'試供品')
195+
group_to_create.domain_name = 'just-has-to-exist'
196+
group = self.server.groups.create_AD_group(group_to_create, False)
197+
self.assertEqual(group.name, u'試供品')
198+
self.assertEqual(group.license_mode, 'onLogin')
199+
self.assertEqual(group.minimum_site_role, 'Creator')
200+
self.assertEqual(group.domain_name, 'active-directory-domain-name')
201+
202+
def test_create_group_async(self):
203+
with open(CREATE_GROUP_ASYNC, 'rb') as f:
204+
response_xml = f.read().decode('utf-8')
205+
with requests_mock.mock() as m:
206+
m.post(self.baseurl, text=response_xml)
207+
group_to_create = TSC.GroupItem(u'試供品')
208+
group_to_create.domain_name = 'woohoo'
209+
job = self.server.groups.create_AD_group(group_to_create, True)
210+
self.assertEqual(job.mode, 'Asynchronous')
211+
self.assertEqual(job.type, 'GroupImport')
212+
188213
def test_update(self):
189214
with open(UPDATE_XML, 'rb') as f:
190215
response_xml = f.read().decode('utf-8')
@@ -197,3 +222,5 @@ def test_update(self):
197222

198223
self.assertEqual('ef8b19c0-43b6-11e6-af50-63f5805dbe3c', group.id)
199224
self.assertEqual('Group updated name', group.name)
225+
self.assertEqual('ExplorerCanPublish', group.minimum_site_role)
226+
self.assertEqual('onLogin', group.license_mode)

0 commit comments

Comments
 (0)
0