8000 Create nested project (#208) · a4441834/server-client-python@0b497d9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0b497d9

Browse files
jimbodrivent8y8
authored andcommitted
Create nested project (tableau#208)
Update Project endpoints and models to support nest projects (under development on Server). This mainly means add `parent_id` to the model, serializers, and parsers. Minor refactoring on how models are parsed in the update call.
1 parent 0d146fa commit 0b497d9

File tree

10 files changed

+112
-32
lines changed

10 files changed

+112
-32
lines changed

docs/docs/samples.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ The following list describes the samples available in the repository:
3434

3535
* `create_group.py`. Create a user group.
3636

37+
* `create_project.py`. Create new projects at the top level as well as nested projects.
38+
3739
* `create_schedules.py`. Create schedules for extract refreshes and subscriptions.
3840

3941
* `explore_datasource.py`. Queries datasources, selects a datasource, populates connections for the datasource, then updates the datasource.
@@ -54,4 +56,3 @@ The following list describes the samples available in the repository:
5456

5557
**Note**: For all of the samples, ensure that your Tableau Server user account has permission to access the resources
5658
requested by the samples.
57-

samples/create_project.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
####
2+
# This script demonstrates how to use the Tableau Server Client
3+
# to create new projects, both at the root level and how to nest them using
4+
# parent_id.
5+
#
6+
#
7+
# To run the script, you must have installed Python 2.7.X or 3.3 and later.
8+
####
9+
10+
import argparse
11+
import getpass
12+
import logging
13+
import sys
14+
15+
import tableauserverclient as TSC
16+
17+
18+
def create_project(server, project_item):
19+
try:
20+
project_item = server.projects.create(project_item)
21+
print('Created a new project called: %s' % project_item.name)
22+
return project_item
23+
except TSC.ServerResponseError:
24+
print('We have already created this project: %s' % project_item.name)
25+
sys.exit(1)
26+
27+
def main():
28+
parser = argparse.ArgumentParser(description='Get all of the refresh tasks available on a server')
29+
parser.add_argument('--server', '-s', required=True, help='server address')
30+
parser.add_argument('--username', '-u', required=True, help='username to sign into server')
31+
parser.add_argument('--site', '-S', default=None)
32+
parser.add_argument('-p', default=None, help='password')
33+
34+
parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
35+
help='desired logging level (set to error by default)')
36+
37+
args = parser.parse_args()
38+
39+
if args.p is None:
40+
password = getpass.getpass("Password: ")
41+
else:
42+
password = args.p
43+
44+
# Set logging level based on user input, or error by default
45+
logging_level = getattr(logging, args.logging_level.upper())
46+
logging.basicConfig(level=logging_level)
47+
48+
tableau_auth = TSC.TableauAuth(args.username, password)
49+
server = TSC.Server(args.server)
50+
51+
with server.auth.sign_in(tableau_auth):
52+
# Use highest Server REST API version available
53+
server.use_server_version()
54+
55+
# Without parent_id specified, projects are created at the top level.
56+
top_level_project = TSC.ProjectItem(name='Top Level Project')
57+
top_level_project = create_project(server, top_level_project)
58+
59+
# Specifying parent_id creates a nested projects.
60+
child_project = TSC.ProjectItem(name='Child Project', parent_id=top_level_project.id)
61+
child_project = create_project(server, child_project)
62+
63+
# Projects can be nested at any level.
64+
grand_child_project = TSC.ProjectItem(name='Grand Child Project', parent_id=child_project.id)
65+
grand_child_project = create_project(server, grand_child_project)
66+
67+
if __name__ == '__main__':
68+
main()

tableauserverclient/models/project_item.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ class ContentPermissions:
88
LockedToProject = 'LockedToProject'
99
ManagedByOwner = 'ManagedByOwner'
1010

11-
def __init__(self, name, description=None, content_permissions=None):
11+
def __init__(self, name, description=None, content_permissions=None, parent_id=None):
1212
self._content_permissions = None
1313
self._id = None
1414
self.description = description
1515
self.name = name
1616
self.content_permissions = content_permissions
17+
self.parent_id = parent_id
1718

1819
@property
1920
def content_permissions(self):
@@ -45,11 +46,11 @@ def _parse_common_tags(self, project_xml):
4546
project_xml = ET.fromstring(project_xml).find('.//t:project', namespaces=NAMESPACE)
4647

4748
if project_xml is not None:
48-
(_, name, description, content_permissions) = self._parse_element(project_xml)
49-
self._set_values(None, name, description, content_permissions)
49+
(_, name, description, content_permissions, parent_id) = self._parse_element(project_xml)
50+
self._set_values(None, name, description, content_permissions, parent_id)
5051
return self
5152

52-
def _set_values(self, project_id, name, description, content_permissions):
53+
def _set_values(self, project_id, name, description, content_permissions, parent_id):
5354
if project_id is not None:
5455
self._id = project_id
5556
if name:
@@ -58,6 +59,8 @@ def _set_values(self, project_id, name, description, content_permissions):
5859
self.description = description
5960
if content_permissions:
6061
self._content_permissions = content_permissions
62+
if parent_id:
63+
self.parent_id = parent_id
6164

6265
@classmethod
6366
def from_response(cls, resp):
@@ -66,9 +69,9 @@ def from_response(cls, resp):
6669
all_project_xml = parsed_response.findall('.//t:project', namespaces=NAMESPACE)
6770

6871
for project_xml in all_project_xml:
69-
(id, name, description, content_permissions) = cls._parse_element(project_xml)
72+
(id, name, description, content_permissions, parent_id) = cls._parse_element(project_xml)
7073
project_item = cls(name)
71-
project_item._set_values(id, name, description, content_permissions)
74+
project_item._set_values(id, name, description, content_permissions, parent_id)
7275
all_project_items.append(project_item)
7376
return all_project_items
7477

@@ -78,5 +81,6 @@ def _parse_element(project_xml):
7881
name = project_xml.get('name', None)
7982
description = project_xml.get('description', None)
8083
content_permissions = project_xml.get('contentPermissions', None)
84+
parent_id = project_xml.get('parentId', None)
8185

82-
return id, name, description, content_permissions
86+
return id, name, description, content_permissions, parent_id

tableauserverclient/server/endpoint/projects_endpoint.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from .exceptions import MissingRequiredFieldError
33
from .. import RequestFactory, ProjectItem, PaginationItem
44
import logging
5-
import copy
65

76
logger = logging.getLogger('tableau.endpoint.projects')
87

@@ -40,8 +39,8 @@ def update(self, project_item):
4039
update_req = RequestFactory.Project.update_req(project_item)
4140
server_response = self.put_request(url, update_req)
4241
logger.info('Updated project item (ID: {0})'.format(project_item.id))
43-
updated_project = copy.copy(project_item)
44-
return updated_project._parse_common_tags(server_response.content)
42+
updated_project = ProjectItem.from_response(server_response.content)[0]
43+
return updated_project
4544

4645
@api(version="2.0")
4746
def create(self, project_item):

tableauserverclient/server/request_factory.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ def create_req(self, project_item):
156156
project_element.attrib['description'] = project_item.description
157157
if project_item.content_permissions:
158158
project_element.attrib['contentPermissions'] = project_item.content_permissions
159+
if project_item.parent_id:
160+
project_element.attrib['parentId'] = project_item.parent_id
159161
return ET.tostring(xml_request)
160162

161163

test/assets/project_create.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<?xml version='1.0' encoding='UTF-8'?>
22
<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-
<project id="ccbea03f-77c4-4209-8774-f67bc59c3cef" name="Test Project" description="Project created for testing" contentPermissions="ManagedByOwner" />
4-
</tsResponse>
3+
<project id="ccbea03f-77c4-4209-8774-f67bc59c3cef" name="Test Project" description="Project created for testing" contentPermissions="ManagedByOwner" parentId="9a8f2265-70f3-4494-96c5-e5949d7a1120" />
4+
</tsResponse>

test/assets/project_get.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<?xml version='1.0' encoding='UTF-8'?>
22
<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="2" />
3+
<pagination pageNumber="1" pageSize="100" totalAvailable="3" />
44
<projects>
55
<project id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" name="default" description="The default project that was automatically created by Tableau." contentPermissions="ManagedByOwner" />
66
<project id="1d0304cd-3796-429f-b815-7258370b9b74" name="Tableau" description="" contentPermissions="ManagedByOwner" />
7+
<project id="4cc52973-5e3a-4d1f-a4fb-5b5f73796edf" name="Tableau > Child 1" description="" contentPermissions="ManagedByOwner" parentId="1d0304cd-3796-429f-b815-7258370b9b74" />
78
</projects>
8-
</tsResponse>
9+
</tsResponse>

test/assets/project_update.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<?xml version='1.0' encoding='UTF-8'?>
22
<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-
<project id="1d0304cd-3796-429f-b815-7258370b9b74" name="Test Project" description="Project created for testing" contentPermissions="LockedToProject" />
4-
</tsResponse>
3+
<project id="1d0304cd-3796-429f-b815-7258370b9b74" name="Test Project" description="Project created for testing" contentPermissions="LockedToProject" parentId="9a8f2265-70f3-4494-96c5-e5949d7a1120" />
4+
</tsResponse>

test/test_project.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,23 @@ def test_get(self):
2727
m.get(self.baseurl, text=response_xml)
2828
all_projects, pagination_item = self.server.projects.get()
2929

30-
self.assertEqual(2, pagination_item.total_available)
30+
self.assertEqual(3, pagination_item.total_available)
3131
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', all_projects[0].id)
3232
self.assertEqual('default', all_projects[0].name)
3333
self.assertEqual('The default project that was automatically created by Tableau.',
3434
all_projects[0].description)
3535
self.assertEqual('ManagedByOwner', all_projects[0].content_permissions)
36+
self.assertEqual(None, all_projects[0].parent_id)
3637

3738
self.assertEqual('1d0304cd-3796-429f-b815-7258370b9b74', all_projects[1].id)
3839
self.assertEqual('Tableau', all_projects[1].name< 10000 /span>)
3940
self.assertEqual('ManagedByOwner', all_projects[1].content_permissions)
41+
self.assertEqual(None, all_projects[1].parent_id)
42+
43+
self.assertEqual('4cc52973-5e3a-4d1f-a4fb-5b5f73796edf', all_projects[2].id)
44+
self.assertEqual('Tableau > Child 1', all_projects[2].name)
45+
self.assertEqual('ManagedByOwner', all_projects[2].content_permissions)
46+
self.assertEqual('1d0304cd-3796-429f-b815-7258370b9b74', all_projects[2].parent_id)
4047

4148
def test_get_before_signin(self):
4249
self.server._auth_token = None
@@ -55,27 +62,18 @@ def test_update(self):
5562
response_xml = f.read().decode('utf-8')
5663
with requests_mock.mock() as m:
5764
m.put(self.baseurl + '/1d0304cd-3796-429f-b815-7258370b9b74', text=response_xml)
58-
single_project = TSC.ProjectItem(name='Test Project', content_permissions='LockedToProject',
59-
description='Project created for testing')
65+
single_project = TSC.ProjectItem(name='Test Project',
66+
content_permissions='LockedToProject',
67+
description='Project created for testing',
68+
parent_id='9a8f2265-70f3-4494-96c5-e5949d7a1120')
6069
single_project._id = '1d0304cd-3796-429f-b815-7258370b9b74'
6170
single_project = self.server.projects.update(single_project)
6271

6372
self.assertEqual('1d0304cd-3796-429f-b815-7258370b9b74', single_project.id)
6473
self.assertEqual('Test Project', single_project.name)
6574
self.assertEqual('Project created for testing', single_project.description)
6675
self.assertEqual('LockedToProject', single_project.content_permissions)
67-
68-
def test_update_copy_fields(self):
69-
with open(UPDATE_XML, 'rb') as f:
70-
response_xml = f.read().decode('utf-8')
71-
with requests_mock.mock() as m:
72-
m.put(self.baseurl + '/1d0304cd-3796-429f-b815-7258370b9b74', text=response_xml)
73-
single_project = TSC.ProjectItem('test')
74-
single_project._id = '1d0304cd-3796-429f-b815-7258370b9b74'
75-
single_project._permissions = 'Test to check if permissions copied over.'
76-
updated_project = self.server.projects.update(single_project)
77-
78-
self.assertEqual(single_project._permissions, updated_project._permissions)
76+
self.assertEqual('9a8f2265-70f3-4494-96c5-e5949d7a1120', single_project.parent_id)
7977

8078
def test_update_missing_id(self):
8179
single_project = TSC.ProjectItem('test')
@@ -88,12 +86,14 @@ def test_create(self):
8886
m.post(self.baseurl, text=response_xml)
8987
new_project = TSC.ProjectItem(name='Test Project', description='Project created for testing')
9088
new_project.content_permissions = 'ManagedByOwner'
89+
new_project.parent_id = '9a8f2265-70f3-4494-96c5-e5949d7a1120'
9190
new_project = self.server.projects.create(new_project)
9291

9392
self.assertEqual('ccbea03f-77c4-4209-8774-f67bc59c3cef', new_project.id)
9493
self.assertEqual('Test Project', new_project.name)
9594
self.assertEqual('Project created for testing', new_project.description)
9695
self.assertEqual('ManagedByOwner', new_project.content_permissions)
96+
self.assertEqual('9a8f2265-70f3-4494-96c5-e5949d7a1120', new_project.parent_id)
9797

9898
def test_create_missing_name(self):
9999
self.assertRaises(ValueError, TSC.ProjectItem, '')

test/test_project_model.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ def test_invalid_content_permissions(self):
1717
project = TSC.ProjectItem("proj")
1818
with self.assertRaises(ValueError):
1919
project.content_permissions = "Hello"
20+
21+
def test_parent_id(self):
22+
project = TSC.ProjectItem("proj")
23+
project.parent_id = "foo"
24+
self.assertEqual(project.parent_id, "foo")

0 commit comments

Comments
 (0)
0