diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index b5b50fe59..c86057a3d 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -5,6 +5,7 @@ from .datasource_item import DatasourceItem from .database_item import DatabaseItem from .exceptions import UnpopulatedPropertyError +from .favorites_item import FavoriteItem from .group_item import GroupItem from .flow_item import FlowItem from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval diff --git a/tableauserverclient/models/favorites_item.py b/tableauserverclient/models/favorites_item.py new file mode 100644 index 000000000..7d2408f93 --- /dev/null +++ b/tableauserverclient/models/favorites_item.py @@ -0,0 +1,49 @@ +import xml.etree.ElementTree as ET +import logging +from .workbook_item import WorkbookItem +from .view_item import ViewItem +from .project_item import ProjectItem +from .datasource_item import DatasourceItem + +logger = logging.getLogger('tableau.models.favorites_item') + + +class FavoriteItem: + class Type: + Workbook = 'workbook' + Datasource = 'datasource' + View = 'view' + Project = 'project' + + @classmethod + def from_response(cls, xml, namespace): + favorites = { + 'datasources': [], + 'projects': [], + 'views': [], + 'workbooks': [], + } + + parsed_response = ET.fromstring(xml) + for workbook in parsed_response.findall('.//t:favorite/t:workbook', namespace): + fav_workbook = WorkbookItem('') + fav_workbook._set_values(*fav_workbook._parse_element(workbook, namespace)) + if fav_workbook: + favorites['workbooks'].append(fav_workbook) + for view in parsed_response.findall('.//t:favorite[t:view]', namespace): + fav_views = ViewItem.from_xml_element(view, namespace) + if fav_views: + for fav_view in fav_views: + favorites['views'].append(fav_view) + for datasource in parsed_response.findall('.//t:favorite/t:datasource', namespace): + fav_datasource = DatasourceItem('') + fav_datasource._set_values(*fav_datasource._parse_element(datasource, namespace)) + if fav_datasource: + favorites['datasources'].append(fav_datasource) + for project in parsed_response.findall('.//t:favorite/t:project', namespace): + fav_project = ProjectItem('p') + fav_project._set_values(*fav_project._parse_element(project)) + if fav_project: + favorites['projects'].append(fav_project) + + return favorites diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py index 3df2004bf..9be38210f 100644 --- a/tableauserverclient/models/user_item.py +++ b/tableauserverclient/models/user_item.py @@ -42,6 +42,7 @@ def __init__(self, name=None, site_role=None, auth_setting=None): self._id = None self._last_login = None self._workbooks = None + self._favorites = None self.email = None self.fullname = None self.name = name @@ -99,6 +100,13 @@ def workbooks(self): raise UnpopulatedPropertyError(error) return self._workbooks() + @property + def favorites(self): + if self._favorites is None: + error = "User item must be populated with favorites first." + raise UnpopulatedPropertyError(error) + return self._favorites + def to_reference(self): return ResourceReference(id_=self.id, tag_name=self.tag_name) diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py index f382d0dba..aff549559 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -8,7 +8,7 @@ PermissionsRule, Permission, ColumnItem, FlowItem, WebhookItem from .endpoint import Auth, Datasources, Endpoint, Groups, Projects, Schedules, \ Sites, Tables, Users, Views, Workbooks, Subscriptions, ServerResponseError, \ - MissingRequiredFieldError, Flows + MissingRequiredFieldError, Flows, Favorites from .server import Server from .pager import Pager from .exceptions import NotSignedInError diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py index fce86f98d..1341ecd3f 100644 --- a/tableauserverclient/server/endpoint/__init__.py +++ b/tableauserverclient/server/endpoint/__init__.py @@ -3,6 +3,7 @@ from .datasources_endpoint import Datasources from .databases_endpoint import Databases from .endpoint import Endpoint +from .favorites_endpoint import Favorites from .flows_endpoint import Flows from .exceptions import ServerResponseError, MissingRequiredFieldError, ServerInfoEndpointNotFoundError from .groups_endpoint import Groups diff --git a/tableauserverclient/server/endpoint/favorites_endpoint.py b/tableauserverclient/server/endpoint/favorites_endpoint.py new file mode 100644 index 000000000..61536ce42 --- /dev/null +++ b/tableauserverclient/server/endpoint/favorites_endpoint.py @@ -0,0 +1,78 @@ +from .endpoint import Endpoint, api +from .exceptions import MissingRequiredFieldError +from .. import RequestFactory +from ...models import FavoriteItem +from ..pager import Pager +import xml.etree.ElementTree as ET +import logging +import copy + +logger = logging.getLogger('tableau.endpoint.favorites') + + +class Favorites(Endpoint): + @property + def baseurl(self): + return "{0}/sites/{1}/favorites".format(self.parent_srv.baseurl, self.parent_srv.site_id) + + # Gets all favorites + @api(version="2.5") + def get(self, user_item, req_options=None): + logger.info('Querying all favorites for user {0}'.format(user_item.name)) + url = '{0}/{1}'.format(self.baseurl, user_item.id) + server_response = self.get_request(url, req_options) + + user_item._favorites = FavoriteItem.from_response(server_response.content, + self.parent_srv.namespace) + + @api(version="2.0") + def add_favorite_workbook(self, user_item, workbook_item): + url = '{0}/{1}'.format(self.baseurl, user_item.id) + add_req = RequestFactory.Favorite.add_workbook_req(workbook_item.id, workbook_item.name) + server_response = self.put_request(url, add_req) + logger.info('Favorited {0} for user (ID: {1})'.format(workbook_item.name, user_item.id)) + + @api(version="2.0") + def add_favorite_view(self, user_item, view_item): + url = '{0}/{1}'.format(self.baseurl, user_item.id) + add_req = RequestFactory.Favorite.add_view_req(view_item.id, view_item.name) + server_response = self.put_request(url, add_req) + logger.info('Favorited {0} for user (ID: {1})'.format(view_item.name, user_item.id)) + + @api(version="2.3") + def add_favorite_datasource(self, user_item, datasource_item): + url = '{0}/{1}'.format(self.baseurl, user_item.id) + add_req = RequestFactory.Favorite.add_datasource_req(datasource_item.id, datasource_item.name) + server_response = self.put_request(url, add_req) + logger.info('Favorited {0} for user (ID: {1})'.format(datasource_item.name, user_item.id)) + + @api(version="3.1") + def add_favorite_project(self, user_item, project_item): + url = '{0}/{1}'.format(self.baseurl, user_item.id) + add_req = RequestFactory.Favorite.add_project_req(project_item.id, project_item.name) + server_response = self.put_request(url, add_req) + logger.info('Favorited {0} for user (ID: {1})'.format(project_item.name, user_item.id)) + + @api(version="2.0") + def delete_favorite_workbook(self, user_item, workbook_item): + url = '{0}/{1}/workbooks/{2}'.format(self.baseurl, user_item.id, workbook_item.id) + logger.info('Removing favorite {0} for user (ID: {1})'.format(workbook_item.id, user_item.id)) + self.delete_request(url) + + @api(version="2.0") + def delete_favorite_view(self, user_item, view_item): + url = '{0}/{1}/views/{2}'.format(self.baseurl, user_item.id, view_item.id) + logger.info('Removing favorite {0} for user (ID: {1})'.format(view_item.id, user_item.id)) + self.delete_request(url) + + @api(version="2.3") + def delete_favorite_datasource(self, user_item, datasource_item): + url = '{0}/{1}/datasources/{2}'.format(self.baseurl, user_item.id, datasource_item.id) + logger.info('Removing favorite {0} for user (ID: {1})'.format(datasource_item.id, user_item.id)) + self.delete_request(url) + + @api(version="3.1") + def delete_favorite_project(self, user_item, project_item): + url = '{0}/{1}/projects/{2}'.format(self.baseurl, user_item.id, project_item.id) + logger.info('Removing favorite {0} for user (ID: {1})'.format(project_item.id, user_item.id)) + self.delete_request(url) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 4307e6496..9c869c686 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -3,7 +3,7 @@ from requests.packages.urllib3.fields import RequestField from requests.packages.urllib3.filepost import encode_multipart_formdata -from ..models import TaskItem +from ..models import TaskItem, UserItem, GroupItem, PermissionsRule, FavoriteItem def _add_multipart(parts): @@ -149,6 +149,34 @@ def publish_req_chunked(self, datasource_item, connection_credentials=None, conn return _add_multipart(parts) +class FavoriteRequest(object): + def _add_to_req(self, id_, target_type, label): + ''' + + + + ''' + xml_request = ET.Element('tsRequest') + favorite_element = ET.SubElement(xml_request, 'favorite') + target = ET.SubElement(favorite_element, target_type) + favorite_element.attrib['label'] = label + target.attrib['id'] = id_ + + return ET.tostring(xml_request) + + def add_datasource_req(self, id_, name): + return self._add_to_req(id_, FavoriteItem.Type.Datasource, name) + + def add_project_req(self, id_, name): + return self._add_to_req(id_, FavoriteItem.Type.Project, name) + + def add_view_req(self, id_, name): + return self._add_to_req(id_, FavoriteItem.Type.View, name) + + def add_workbook_req(self, id_, name): + return self._add_to_req(id_, FavoriteItem.Type.Workbook, name) + + class FileuploadRequest(object): def chunk_req(self, chunk): parts = {'request_payload': ('', '', 'text/xml'), @@ -605,6 +633,7 @@ class RequestFactory(object): Datasource = DatasourceRequest() Database = DatabaseRequest() Empty = EmptyRequest() + Favorite = FavoriteRequest() Fileupload = FileuploadRequest() Flow = FlowRequest() Group = GroupRequest() diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index 37371d707..c36ee0f4b 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -4,7 +4,7 @@ from ..namespace import Namespace from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, \ Schedules, ServerInfo, Tasks, Subscriptions, Jobs, Metadata,\ - Databases, Tables, Flows, Webhooks, DataAccelerationReport + Databases, Tables, Flows, Webhooks, DataAccelerationReport, Favorites from .endpoint.exceptions import EndpointUnavailableError, ServerInfoEndpointNotFoundError import requests @@ -46,6 +46,7 @@ def __init__(self, server_address, use_server_version=False): self.jobs = Jobs(self) self.workbooks = Workbooks(self) self.datasources = Datasources(self) + self.favorites = Favorites(self) self.flows = Flows(self) self.projects = Projects(self) self.schedules = Schedules(self) diff --git a/test/assets/favorites_add_datasource.xml b/test/assets/favorites_add_datasource.xml new file mode 100644 index 000000000..a1f47ab4f --- /dev/null +++ b/test/assets/favorites_add_datasource.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/assets/favorites_add_project.xml b/test/assets/favorites_add_project.xml new file mode 100644 index 000000000..699e6a4cd --- /dev/null +++ b/test/assets/favorites_add_project.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/test/assets/favorites_add_view.xml b/test/assets/favorites_add_view.xml new file mode 100644 index 000000000..f6fc15c9a --- /dev/null +++ b/test/assets/favorites_add_view.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/assets/favorites_add_workbook.xml b/test/assets/favorites_add_workbook.xml new file mode 100644 index 000000000..c8008c9b8 --- /dev/null +++ b/test/assets/favorites_add_workbook.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/assets/favorites_get.xml b/test/assets/favorites_get.xml new file mode 100644 index 000000000..3d2e2ee6a --- /dev/null +++ b/test/assets/favorites_get.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/test_favorites.py b/test/test_favorites.py new file mode 100644 index 000000000..f76517b64 --- /dev/null +++ b/test/test_favorites.py @@ -0,0 +1,129 @@ +import unittest +import os +import requests_mock +import xml.etree.ElementTree as ET +import tableauserverclient as TSC +from tableauserverclient.datetime_helpers import format_datetime +from tableauserverclient.server.endpoint.exceptions import InternalServerError +from tableauserverclient.server.request_factory import RequestFactory +from ._utils import read_xml_asset, read_xml_assets, asset + +GET_FAVORITES_XML = 'favorites_get.xml' +ADD_FAVORITE_WORKBOOK_XML = 'favorites_add_workbook.xml' +ADD_FAVORITE_VIEW_XML = 'favorites_add_view.xml' +ADD_FAVORITE_DATASOURCE_XML = 'favorites_add_datasource.xml' +ADD_FAVORITE_PROJECT_XML = 'favorites_add_project.xml' + + +class FavoritesTests(unittest.TestCase): + def setUp(self): + self.server = TSC.Server('http://test') + self.server.version = '2.5' + + # Fake signin + self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67' + self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM' + + self.baseurl = self.server.favorites.baseurl + self.user = TSC.UserItem('alice', TSC.UserItem.Roles.Viewer) + self.user._id = 'dd2239f6-ddf1-4107-981a-4cf94e415794' + + def test_get(self): + response_xml = read_xml_asset(GET_FAVORITES_XML) + with requests_mock.mock() as m: + m.get('{0}/{1}'.format(self.baseurl, self.user.id), + text=response_xml) + self.server.favorites.get(self.user) + self.assertIsNotNone(self.user._favorites) + self.assertEqual(len(self.user.favorites['workbooks']), 1) + self.assertEqual(len(self.user.favorites['views']), 1) + self.assertEqual(len(self.user.favorites['projects']), 1) + self.assertEqual(len(self.user.favorites['datasources']), 1) + + workbook = self.user.favorites['workbooks'][0] + view = self.user.favorites['views'][0] + datasource = self.user.favorites['datasources'][0] + project = self.user.favorites['projects'][0] + + self.assertEqual(workbook.id, '6d13b0ca-043d-4d42-8c9d-3f3313ea3a00') + self.assertEqual(view.id, 'd79634e1-6063-4ec9-95ff-50acbf609ff5') + self.assertEqual(datasource.id, 'e76a1461-3b1d-4588-bf1b-17551a879ad9') + self.assertEqual(project.id, '1d0304cd-3796-429f-b815-7258370b9b74') + + def test_add_favorite_workbook(self): + response_xml = read_xml_asset(ADD_FAVORITE_WORKBOOK_XML) + workbook = TSC.WorkbookItem('') + workbook._id = '6d13b0ca-043d-4d42-8c9d-3f3313ea3a00' + workbook.name = 'Superstore' + with requests_mock.mock() as m: + m.put('{0}/{1}'.format(self.baseurl, self.user.id), + text=response_xml) + self.server.favorites.add_favorite_workbook(self.user, workbook) + + def test_add_favorite_view(self): + response_xml = read_xml_asset(ADD_FAVORITE_VIEW_XML) + view = TSC.ViewItem() + view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5' + view._name = 'ENDANGERED SAFARI' + with requests_mock.mock() as m: + m.put('{0}/{1}'.format(self.baseurl, self.user.id), + text=response_xml) + self.server.favorites.add_favorite_view(self.user, view) + + def test_add_favorite_datasource(self): + response_xml = read_xml_asset(ADD_FAVORITE_DATASOURCE_XML) + datasource = TSC.DatasourceItem('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760') + datasource._id = 'e76a1461-3b1d-4588-bf1b-17551a879ad9' + datasource.name = 'SampleDS' + with requests_mock.mock() as m: + m.put('{0}/{1}'.format(self.baseurl, self.user.id), + text=response_xml) + self.server.favorites.add_favorite_datasource(self.user, datasource) + + def test_add_favorite_project(self): + self.server.version = '3.1' + baseurl = self.server.favorites.baseurl + response_xml = read_xml_asset(ADD_FAVORITE_PROJECT_XML) + project = TSC.ProjectItem('Tableau') + project._id = '1d0304cd-3796-429f-b815-7258370b9b74' + with requests_mock.mock() as m: + m.put('{0}/{1}'.format(baseurl, self.user.id), + text=response_xml) + self.server.favorites.add_favorite_project(self.user, project) + + def test_delete_favorite_workbook(self): + workbook = TSC.WorkbookItem('') + workbook._id = '6d13b0ca-043d-4d42-8c9d-3f3313ea3a00' + workbook.name = 'Superstore' + with requests_mock.mock() as m: + m.delete('{0}/{1}/workbooks/{2}'.format(self.baseurl, self.user.id, + workbook.id)) + self.server.favorites.delete_favorite_workbook(self.user, workbook) + + def test_delete_favorite_view(self): + view = TSC.ViewItem() + view._id = 'd79634e1-6063-4ec9-95ff-50acbf609ff5' + view._name = 'ENDANGERED SAFARI' + with requests_mock.mock() as m: + m.delete('{0}/{1}/views/{2}'.format(self.baseurl, self.user.id, + view.id)) + self.server.favorites.delete_favorite_view(self.user, view) + + def test_delete_favorite_datasource(self): + datasource = TSC.DatasourceItem('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760') + datasource._id = 'e76a1461-3b1d-4588-bf1b-17551a879ad9' + datasource.name = 'SampleDS' + with requests_mock.mock() as m: + m.delete('{0}/{1}/datasources/{2}'.format(self.baseurl, self.user.id, + datasource.id)) + self.server.favorites.delete_favorite_datasource(self.user, datasource) + + def test_delete_favorite_project(self): + self.server.version = '3.1' + baseurl = self.server.favorites.baseurl + project = TSC.ProjectItem('Tableau') + project._id = '1d0304cd-3796-429f-b815-7258370b9b74' + with requests_mock.mock() as m: + m.delete('{0}/{1}/projects/{2}'.format(baseurl, self.user.id, + project.id)) + self.server.favorites.delete_favorite_project(self.user, project) diff --git a/test/test_project.py b/test/test_project.py index b57d52df5..5e9869c6e 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -100,14 +100,14 @@ def test_update_datasource_default_permission(self): new_rules = self.server.projects.update_datasource_default_permissions(project, rules) - self.assertEquals('b4488bce-80f0-11ea-af1c-976d0c1dab39', new_rules[0].grantee.id) + self.assertEqual('b4488bce-80f0-11ea-af1c-976d0c1dab39', new_rules[0].grantee.id) updated_capabilities = new_rules[0].capabilities - self.assertEquals(4, len(updated_capabilities)) - self.assertEquals('Deny', updated_capabilities['ExportXml']) - self.assertEquals('Allow', updated_capabilities['Read']) - self.assertEquals('Allow', updated_capabilities['Write']) - self.assertEquals('Allow', updated_capabilities['Connect']) + self.assertEqual(4, len(updated_capabilities)) + self.assertEqual('Deny', updated_capabilities['ExportXml']) + self.assertEqual('Allow', updated_capabilities['Read']) + self.assertEqual('Allow', updated_capabilities['Write']) + self.assertEqual('Allow', updated_capabilities['Connect']) def test_update_missing_id(self): single_project = TSC.ProjectItem('test')