Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new fields to ProjectItem and clean up parsing logic #1544

Open
wants to merge 6 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 70 additions & 42 deletions tableauserverclient/models/project_item.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import logging
import xml.etree.ElementTree as ET
from .property_decorators import property_is_enum, property_not_empty, property_not_nullable
from ..datetime_helpers import parse_datetime
from typing import Optional

from defusedxml.ElementTree import fromstring
Expand Down Expand Up @@ -38,6 +40,11 @@ def __init__(
self._samples: Optional[bool] = samples
self._owner_id: Optional[str] = None

self._created_at = None
self._owner_name: Optional[str] = None
self._top_level_project: Optional[bool] = None
self._updated_at = None

self._permissions = None
self._default_workbook_permissions = None
self._default_datasource_permissions = None
Expand Down Expand Up @@ -122,6 +129,18 @@ def default_table_permissions(self):
def id(self) -> Optional[str]:
return self._id

@property
def created_at(self):
return self._created_at

@property
def description(self):
return self._description

@description.setter
def description(self, value):
self._description = value

@property
def name(self) -> Optional[str]:
return self._name
Expand All @@ -130,6 +149,26 @@ def name(self) -> Optional[str]:
def name(self, value: str) -> None:
self._name = value

@property
def owner_name(self):
return self._owner_name

@property
def parent_id(self):
return self._parent_id

@parent_id.setter
def parent_id(self, value):
self._parent_id = value

@property
def top_level_project(self):
return self._top_level_project

@property
def updated_at(self):
return self._updated_at

@property
def owner_id(self) -> Optional[str]:
return self._owner_id
Expand All @@ -142,33 +181,7 @@ def is_default(self):
return self.name.lower() == "default"

def _parse_common_tags(self, project_xml, ns):
if not isinstance(project_xml, ET.Element):
project_xml = fromstring(project_xml).find(".//t:project", namespaces=ns)

if project_xml is not None:
(
_,
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, parent_id, owner_id):
if project_id is not None:
self._id = project_id
if name:
self._name = name
if description:
self.description = description
if content_permissions:
self._content_permissions = content_permissions
if parent_id:
self.parent_id = parent_id
if owner_id:
self._owner_id = owner_id
return ProjectItem.from_xml(ProjectItem, project_xml, ns)

def _set_permissions(self, permissions):
self._permissions = permissions
Expand All @@ -188,25 +201,40 @@ def from_response(cls, resp, ns) -> list["ProjectItem"]:
all_project_xml = parsed_response.findall(".//t:project", namespaces=ns)

for project_xml in all_project_xml:
project_item = cls.from_xml(project_xml)
project_item = cls.from_xml(project_xml, ns)
all_project_items.append(project_item)
return all_project_items

@classmethod
def from_xml(cls, project_xml, namespace=None) -> "ProjectItem":
def from_xml(cls, project_xml, ns) -> "ProjectItem":
project_item = cls()
project_item._set_values(*cls._parse_element(project_xml))
if project_xml.get("contentPermissions", None):
project_item._content_permissions = project_xml.get("contentPermissions")
if project_xml.get("createdAt", None):
project_item._created_at = parse_datetime(project_xml.get("createdAt"))
if project_xml.get("description", None):
project_item._description = project_xml.get("description")
if project_xml.get("id", None):
project_item._id = project_xml.get("id")
if project_xml.get("name", None):
project_item._name = project_xml.get("name")
if project_xml.get("parentProjectId", None):
project_item._parent_id = project_xml.get("parentProjectId")
if project_xml.get("topLevelProject", None):
project_item._top_level_project = ProjectItem.string_to_bool(project_xml.get("topLevelProject"))
if project_xml.get("updatedAt", None):
project_item._updated_at = parse_datetime(project_xml.get("updatedAt"))

if project_item.parent_id is not None:
project_item._top_level_project = False

owner_elem = project_xml.find(".//t:owner", ns)
if owner_elem is not None:
project_item._owner_id = owner_elem.get("id", None)
project_item._owner_name = owner_elem.get("name", None)

return project_item

@staticmethod
def _parse_element(project_xml):
id = project_xml.get("id", None)
name = project_xml.get("name", None)
description = project_xml.get("description", None)
content_permissions = project_xml.get("contentPermissions", None)
parent_id = project_xml.get("parentProjectId", None)
owner_id = None
for owner in project_xml:
owner_id = owner.get("id", None)

return id, name, description, content_permissions, parent_id, owner_id
# Used to convert string represented boolean to a boolean type
def string_to_bool(s):
return s.lower() == "true"
11 changes: 7 additions & 4 deletions tableauserverclient/server/endpoint/projects_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from .endpoint import Endpoint, api, parameter_added_in
from .exceptions import MissingRequiredFieldError

import logging

from tableauserverclient.server.endpoint.default_permissions_endpoint import _DefaultPermissionsEndpoint
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api, XML_CONTENT_TYPE
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
from tableauserverclient.server import RequestFactory, RequestOptions
from tableauserverclient.models.permissions_item import PermissionsRule
from tableauserverclient.models import ProjectItem, PaginationItem, Resource
from tableauserverclient.models import PaginationItem, PermissionsRule, ProjectItem, Resource

from typing import Optional, TYPE_CHECKING

Expand All @@ -33,7 +35,7 @@ def baseurl(self) -> str:
@api(version="2.0")
def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[ProjectItem], PaginationItem]:
logger.info("Querying all projects on site")
url = self.baseurl
url = self.baseurl + "?fields=_all_"
server_response = self.get_request(url, req_options)
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
all_project_items = ProjectItem.from_response(server_response.content, self.parent_srv.namespace)
Expand All @@ -49,13 +51,14 @@ def delete(self, project_id: str) -> None:
logger.info(f"Deleted single project (ID: {project_id})")

@api(version="2.0")
@parameter_added_in(publish_samples="2.5")
def update(self, project_item: ProjectItem, samples: bool = False) -> ProjectItem:
if not project_item.id:
error = "Project item missing ID."
raise MissingRequiredFieldError(error)

params = {"params": {RequestOptions.Field.PublishSamples: samples}}
url = f"{self.baseurl}/{project_item.id}"
params = {"params": {RequestOptions.Field.PublishSamples: samples}}
update_req = RequestFactory.Project.update_req(project_item)
server_response = self.put_request(url, update_req, XML_CONTENT_TYPE, params)
logger.info(f"Updated project item (ID: {project_item.id})")
Expand Down
12 changes: 7 additions & 5 deletions test/assets/project_get.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<?xml version='1.0' encoding='UTF-8'?>
<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">
<pagination pageNumber="1" pageSize="100" totalAvailable="3" />
<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-3.4.xsd">
<pagination pageNumber="1" pageSize="100" totalAvailable="3"/>
<projects>
<project id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" name="default" description="The default project that was automatically created by Tableau." contentPermissions="ManagedByOwner"><owner id="dd2239f6-ddf1-4107-981a-4cf94e415794" /></project>
<project id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" name="default" description="The default project that was automatically created by Tableau." contentPermissions="ManagedByOwner"><owner id="dd2239f6-ddf1-4107-981a-4cf94e415794" name="_system" siteRole="Unlicensed"/>
</project>
<project id="1d0304cd-3796-429f-b815-7258370b9b74" name="Tableau" description="" contentPermissions="ManagedByOwner"><owner id="2a47bbf8-8900-4ebb-b0a4-2723bd7c46c3" /></project>
<project id="4cc52973-5e3a-4d1f-a4fb-5b5f73796edf" name="Tableau > Child 1" description="" contentPermissions="ManagedByOwner" parentProjectId="1d0304cd-3796-429f-b815-7258370b9b74"><owner id="dd2239f6-ddf1-4107-981a-4cf94e415794" /></project>
<project id="4cc52973-5e3a-4d1f-a4fb-5b5f73796edf" name="Tableau > Child 1" description="" contentPermissions="ManagedByOwner" parentProjectId="1d0304cd-3796-429f-b815-7258370b9b74"><owner id="dd2239f6-ddf1-4107-981a-4cf94e415794" name="testadmin" fullName="testadmin" siteRole="ServerAdministrator" lastLogin="2019-03-14T17:12:50Z"/>
</project>
</projects>
</tsResponse>
</tsResponse>
3 changes: 3 additions & 0 deletions test/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import requests_mock

import tableauserverclient as TSC
from tableauserverclient.datetime_helpers import format_datetime
from tableauserverclient import GroupItem
from ._utils import read_xml_asset, asset

Expand Down Expand Up @@ -42,6 +43,7 @@ def test_get(self) -> None:
self.assertEqual("ManagedByOwner", all_projects[0].content_permissions)
self.assertEqual(None, all_projects[0].parent_id)
self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", all_projects[0].owner_id)
self.assertEqual("_system", all_projects[0].owner_name)

self.assertEqual("1d0304cd-3796-429f-b815-7258370b9b74", all_projects[1].id)
self.assertEqual("Tableau", all_projects[1].name)
Expand All @@ -54,6 +56,7 @@ def test_get(self) -> None:
self.assertEqual("ManagedByOwner", all_projects[2].content_permissions)
self.assertEqual("1d0304cd-3796-429f-b815-7258370b9b74", all_projects[2].parent_id)
self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", all_projects[2].owner_id)
self.assertEqual("testadmin", all_projects[2].owner_name)

def test_get_before_signin(self) -> None:
self.server._auth_token = None
Expand Down
Loading