diff --git a/docs/docs/samples.md b/docs/docs/samples.md
index cf31c1882..875744433 100644
--- a/docs/docs/samples.md
+++ b/docs/docs/samples.md
@@ -34,6 +34,8 @@ The following list describes the samples available in the repository:
* `create_group.py`. Create a user group.
+* `create_project.py`. Create new projects at the top level as well as nested projects.
+
* `create_schedules.py`. Create schedules for extract refreshes and subscriptions.
* `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:
**Note**: For all of the samples, ensure that your Tableau Server user account has permission to access the resources
requested by the samples.
-
diff --git a/samples/create_project.py b/samples/create_project.py
new file mode 100644
index 000000000..c68b992a9
--- /dev/null
+++ b/samples/create_project.py
@@ -0,0 +1,68 @@
+####
+# This script demonstrates how to use the Tableau Server Client
+# to create new projects, both at the root level and how to nest them using
+# parent_id.
+#
+#
+# To run the script, you must have installed Python 2.7.X or 3.3 and later.
+####
+
+import argparse
+import getpass
+import logging
+import sys
+
+import tableauserverclient as TSC
+
+
+def create_project(server, project_item):
+ try:
+ project_item = server.projects.create(project_item)
+ print('Created a new project called: %s' % project_item.name)
+ return project_item
+ except TSC.ServerResponseError:
+ print('We have already created this project: %s' % project_item.name)
+ sys.exit(1)
+
+def main():
+ parser = argparse.ArgumentParser(description='Get all of the refresh tasks available on a server')
+ parser.add_argument('--server', '-s', required=True, help='server address')
+ parser.add_argument('--username', '-u', required=True, help='username to sign into server')
+ parser.add_argument('--site', '-S', default=None)
+ parser.add_argument('-p', default=None, help='password')
+
+ parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
+ help='desired logging level (set to error by default)')
+
+ args = parser.parse_args()
+
+ if args.p is None:
+ password = getpass.getpass("Password: ")
+ else:
+ password = args.p
+
+ # Set logging level based on user input, or error by default
+ logging_level = getattr(logging, args.logging_level.upper())
+ logging.basicConfig(level=logging_level)
+
+ tableau_auth = TSC.TableauAuth(args.username, password)
+ server = TSC.Server(args.server)
+
+ with server.auth.sign_in(tableau_auth):
+ # Use highest Server REST API version available
+ server.use_server_version()
+
+ # Without parent_id specified, projects are created at the top level.
+ top_level_project = TSC.ProjectItem(name='Top Level Project')
+ top_level_project = create_project(server, top_level_project)
+
+ # Specifying parent_id creates a nested projects.
+ child_project = TSC.ProjectItem(name='Child Project', parent_id=top_level_project.id)
+ child_project = create_project(server, child_project)
+
+ # Projects can be nested at any level.
+ grand_child_project = TSC.ProjectItem(name='Grand Child Project', parent_id=child_project.id)
+ grand_child_project = create_project(server, grand_child_project)
+
+if __name__ == '__main__':
+ main()
diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py
index b60a62633..79ad287a9 100644
--- a/tableauserverclient/models/project_item.py
+++ b/tableauserverclient/models/project_item.py
@@ -8,12 +8,13 @@ class ContentPermissions:
LockedToProject = 'LockedToProject'
ManagedByOwner = 'ManagedByOwner'
- def __init__(self, name, description=None, content_permissions=None):
+ def __init__(self, name, description=None, content_permissions=None, parent_id=None):
self._content_permissions = None
self._id = None
self.description = description
self.name = name
self.content_permissions = content_permissions
+ self.parent_id = parent_id
@property
def content_permissions(self):
@@ -45,11 +46,11 @@ def _parse_common_tags(self, project_xml):
project_xml = ET.fromstring(project_xml).find('.//t:project', namespaces=NAMESPACE)
if project_xml is not None:
- (_, name, description, content_permissions) = self._parse_element(project_xml)
- self._set_values(None, name, description, content_permissions)
+ (_, name, description, content_permissions, parent_id) = self._parse_element(project_xml)
+ self._set_values(None, name, description, content_permissions, parent_id)
return self
- def _set_values(self, project_id, name, description, content_permissions):
+ def _set_values(self, project_id, name, description, content_permissions, parent_id):
if project_id is not None:
self._id = project_id
if name:
@@ -58,6 +59,8 @@ def _set_values(self, project_id, name, description, content_permissions):
self.description = description
if content_permissions:
self._content_permissions = content_permissions
+ if parent_id:
+ self.parent_id = parent_id
@classmethod
def from_response(cls, resp):
@@ -66,9 +69,9 @@ def from_response(cls, resp):
all_project_xml = parsed_response.findall('.//t:project', namespaces=NAMESPACE)
for project_xml in all_project_xml:
- (id, name, description, content_permissions) = cls._parse_element(project_xml)
+ (id, name, description, content_permissions, parent_id) = cls._parse_element(project_xml)
project_item = cls(name)
- project_item._set_values(id, name, description, content_permissions)
+ project_item._set_values(id, name, description, content_permissions, parent_id)
all_project_items.append(project_item)
return all_project_items
@@ -78,5 +81,6 @@ def _parse_element(project_xml):
name = project_xml.get('name', None)
description = project_xml.get('description', None)
content_permissions = project_xml.get('contentPermissions', None)
+ parent_id = project_xml.get('parentId', None)
- return id, name, description, content_permissions
+ return id, name, description, content_permissions, parent_id
diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py
index 77d57a88a..32b485269 100644
--- a/tableauserverclient/server/endpoint/projects_endpoint.py
+++ b/tableauserverclient/server/endpoint/projects_endpoint.py
@@ -2,7 +2,6 @@
from .exceptions import MissingRequiredFieldError
from .. import RequestFactory, ProjectItem, PaginationItem
import logging
-import copy
logger = logging.getLogger('tableau.endpoint.projects')
@@ -40,8 +39,8 @@ def update(self, project_item):
update_req = RequestFactory.Project.update_req(project_item)
server_response = self.put_request(url, update_req)
logger.info('Updated project item (ID: {0})'.format(project_item.id))
- updated_project = copy.copy(project_item)
- return updated_project._parse_common_tags(server_response.content)
+ updated_project = ProjectItem.from_response(server_response.content)[0]
+ return updated_project
@api(version="2.0")
def create(self, project_item):
diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py
index 9dbaaacba..44bb6e749 100644
--- a/tableauserverclient/server/request_factory.py
+++ b/tableauserverclient/server/request_factory.py
@@ -156,6 +156,8 @@ def create_req(self, project_item):
project_element.attrib['description'] = project_item.description
if project_item.content_permissions:
project_element.attrib['contentPermissions'] = project_item.content_permissions
+ if project_item.parent_id:
+ project_element.attrib['parentId'] = project_item.parent_id
return ET.tostring(xml_request)
diff --git a/test/assets/project_create.xml b/test/assets/project_create.xml
index a6584b672..ebfada762 100644
--- a/test/assets/project_create.xml
+++ b/test/assets/project_create.xml
@@ -1,4 +1,4 @@
-
-
\ No newline at end of file
+
+
diff --git a/test/assets/project_get.xml b/test/assets/project_get.xml
index 941193f6f..12133f432 100644
--- a/test/assets/project_get.xml
+++ b/test/assets/project_get.xml
@@ -1,8 +1,9 @@
-
+
+
-
\ No newline at end of file
+
diff --git a/test/assets/project_update.xml b/test/assets/project_update.xml
index bc516fc64..0307e7c18 100644
--- a/test/assets/project_update.xml
+++ b/test/assets/project_update.xml
@@ -1,4 +1,4 @@
-
-
\ No newline at end of file
+
+
diff --git a/test/test_project.py b/test/test_project.py
index a099e1a11..c0958f761 100644
--- a/test/test_project.py
+++ b/test/test_project.py
@@ -27,16 +27,23 @@ def test_get(self):
m.get(self.baseurl, text=response_xml)
all_projects, pagination_item = self.server.projects.get()
- self.assertEqual(2, pagination_item.total_available)
+ self.assertEqual(3, pagination_item.total_available)
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', all_projects[0].id)
self.assertEqual('default', all_projects[0].name)
self.assertEqual('The default project that was automatically created by Tableau.',
all_projects[0].description)
self.assertEqual('ManagedByOwner', all_projects[0].content_permissions)
+ self.assertEqual(None, all_projects[0].parent_id)
self.assertEqual('1d0304cd-3796-429f-b815-7258370b9b74', all_projects[1].id)
self.assertEqual('Tableau', all_projects[1].name)
self.assertEqual('ManagedByOwner', all_projects[1].content_permissions)
+ self.assertEqual(None, all_projects[1].parent_id)
+
+ self.assertEqual('4cc52973-5e3a-4d1f-a4fb-5b5f73796edf', all_projects[2].id)
+ self.assertEqual('Tableau > Child 1', all_projects[2].name)
+ self.assertEqual('ManagedByOwner', all_projects[2].content_permissions)
+ self.assertEqual('1d0304cd-3796-429f-b815-7258370b9b74', all_projects[2].parent_id)
def test_get_before_signin(self):
self.server._auth_token = None
@@ -55,8 +62,10 @@ def test_update(self):
response_xml = f.read().decode('utf-8')
with requests_mock.mock() as m:
m.put(self.baseurl + '/1d0304cd-3796-429f-b815-7258370b9b74', text=response_xml)
- single_project = TSC.ProjectItem(name='Test Project', content_permissions='LockedToProject',
- description='Project created for testing')
+ single_project = TSC.ProjectItem(name='Test Project',
+ content_permissions='LockedToProject',
+ description='Project created for testing',
+ parent_id='9a8f2265-70f3-4494-96c5-e5949d7a1120')
single_project._id = '1d0304cd-3796-429f-b815-7258370b9b74'
single_project = self.server.projects.update(single_project)
@@ -64,18 +73,7 @@ def test_update(self):
self.assertEqual('Test Project', single_project.name)
self.assertEqual('Project created for testing', single_project.description)
self.assertEqual('LockedToProject', single_project.content_permissions)
-
- def test_update_copy_fields(self):
- with open(UPDATE_XML, 'rb') as f:
- response_xml = f.read().decode('utf-8')
- with requests_mock.mock() as m:
- m.put(self.baseurl + '/1d0304cd-3796-429f-b815-7258370b9b74', text=response_xml)
- single_project = TSC.ProjectItem('test')
- single_project._id = '1d0304cd-3796-429f-b815-7258370b9b74'
- single_project._permissions = 'Test to check if permissions copied over.'
- updated_project = self.server.projects.update(single_project)
-
- self.assertEqual(single_project._permissions, updated_project._permissions)
+ self.assertEqual('9a8f2265-70f3-4494-96c5-e5949d7a1120', single_project.parent_id)
def test_update_missing_id(self):
single_project = TSC.ProjectItem('test')
@@ -88,12 +86,14 @@ def test_create(self):
m.post(self.baseurl, text=response_xml)
new_project = TSC.ProjectItem(name='Test Project', description='Project created for testing')
new_project.content_permissions = 'ManagedByOwner'
+ new_project.parent_id = '9a8f2265-70f3-4494-96c5-e5949d7a1120'
new_project = self.server.projects.create(new_project)
self.assertEqual('ccbea03f-77c4-4209-8774-f67bc59c3cef', new_project.id)
self.assertEqual('Test Project', new_project.name)
self.assertEqual('Project created for testing', new_project.description)
self.assertEqual('ManagedByOwner', new_project.content_permissions)
+ self.assertEqual('9a8f2265-70f3-4494-96c5-e5949d7a1120', new_project.parent_id)
def test_create_missing_name(self):
self.assertRaises(ValueError, TSC.ProjectItem, '')
diff --git a/test/test_project_model.py b/test/test_project_model.py
index 3ab14b3f6..56e6c3d11 100644
--- a/test/test_project_model.py
+++ b/test/test_project_model.py
@@ -17,3 +17,8 @@ def test_invalid_content_permissions(self):
project = TSC.ProjectItem("proj")
with self.assertRaises(ValueError):
project.content_permissions = "Hello"
+
+ def test_parent_id(self):
+ project = TSC.ProjectItem("proj")
+ project.parent_id = "foo"
+ self.assertEqual(project.parent_id, "foo")