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")