From d7b677e022f2f271c77d59ca92cb31fffd674014 Mon Sep 17 00:00:00 2001 From: Michael Harbarth Date: Mon, 22 Jul 2024 18:52:08 +0200 Subject: [PATCH 1/4] feat: Reimplement layout rendering options and add support for outlineNumbering --- polarion_rest_api_client/clients/documents.py | 30 +++++- polarion_rest_api_client/data_models.py | 87 ++++++++++++++++- pyproject.toml | 2 +- tests/conftest.py | 1 + .../expected_requests/create_document.json | 2 +- .../expected_requests/update_document.json | 2 +- .../expected_requests/update_document_2.json | 1 + .../update_document_2.json.license | 2 + .../data/mock_api_responses/get_document.json | 30 +++++- tests/test_client_documents.py | 95 +++++++++++++++++-- 10 files changed, 235 insertions(+), 17 deletions(-) create mode 100644 tests/data/expected_requests/update_document_2.json create mode 100644 tests/data/expected_requests/update_document_2.json.license diff --git a/polarion_rest_api_client/clients/documents.py b/polarion_rest_api_client/clients/documents.py index 81988c4d..b0916a3b 100644 --- a/polarion_rest_api_client/clients/documents.py +++ b/polarion_rest_api_client/clients/documents.py @@ -98,6 +98,14 @@ def get( home_page_content=home_page_content, title=self.unset_to_none(attributes.title), rendering_layouts=rendering_layouts, + outline_numbering=self.unset_to_none( + attributes.uses_outline_numbering + ), + outline_numbering_prefix=( + self.unset_to_none(attributes.outline_numbering.prefix) + if attributes.outline_numbering + else None + ), ) return None @@ -143,7 +151,7 @@ def _update(self, to_update: dm.Document | list[dm.Document]): api_models.DocumentsSinglePatchRequestDataAttributesRenderingLayoutsItemPropertiesItem.from_dict( p ) - for p in layout.properties + for p in layout.properties.serialize() ] if layout.properties else oa_types.UNSET @@ -154,6 +162,15 @@ def _update(self, to_update: dm.Document | list[dm.Document]): if to_update.rendering_layouts else oa_types.UNSET ), + uses_outline_numbering=to_update.outline_numbering + or oa_types.UNSET, + outline_numbering=( + api_models.DocumentsSinglePatchRequestDataAttributesOutlineNumbering( + prefix=to_update.outline_numbering_prefix + ) + if to_update.outline_numbering_prefix + else oa_types.UNSET + ), ), ) ) @@ -211,7 +228,7 @@ def _create(self, items: list[dm.Document]): api_models.DocumentsListPostRequestDataItemAttributesRenderingLayoutsItemPropertiesItem.from_dict( p ) - for p in layout.properties + for p in layout.properties.serialize() ] if layout.properties else oa_types.UNSET @@ -222,6 +239,15 @@ def _create(self, items: list[dm.Document]): if document.rendering_layouts else oa_types.UNSET ), + uses_outline_numbering=document.outline_numbering + or oa_types.UNSET, + outline_numbering=( + api_models.DocumentsListPostRequestDataItemAttributesOutlineNumbering( + prefix=document.outline_numbering_prefix + ) + if document.outline_numbering_prefix + else oa_types.UNSET + ), ), ) for document in items diff --git a/polarion_rest_api_client/data_models.py b/polarion_rest_api_client/data_models.py index c4eb4be9..58ff2c09 100644 --- a/polarion_rest_api_client/data_models.py +++ b/polarion_rest_api_client/data_models.py @@ -11,6 +11,19 @@ import json import typing as t +field_at_start: list[str] | None = None +fields_at_end: list[str] | None = None +sidebar_work_item_fields: list[str] | None = None +fields_at_end_as_table: bool = False + +RENDERING_LAYOUT_FIELDS = { + "fieldsAtEndAsTable": "fields_at_end_as_table", + "fieldsAtStart": "fields_at_start", + "fieldsAtEnd": "fields_at_end", + "sidebarWorkItemFields": "sidebar_work_item_fields", + "hidden": "hidden", +} + @dataclasses.dataclass class StatusItem: @@ -215,6 +228,8 @@ class Document(StatusItem): module_name: str | None = None home_page_content: TextContent | None = None title: str | None = None + outline_numbering: bool | None = None + outline_numbering_prefix: str | None = None def __init__( self, @@ -226,6 +241,8 @@ def __init__( home_page_content: TextContent | None = None, title: str | None = None, rendering_layouts: list[RenderingLayout] | None = None, + outline_numbering: bool | None = None, + outline_numbering_prefix: str | None = None, ): super().__init__(id, type, status) self.module_folder = module_folder @@ -233,6 +250,12 @@ def __init__( self.home_page_content = home_page_content self.title = title self.rendering_layouts = rendering_layouts + self.outline_numbering = outline_numbering + self.outline_numbering_prefix = outline_numbering_prefix + + def __eq__(self, other): + """Compare dicts instead of hashes.""" + return self.__dict__ == other.__dict__ @dataclasses.dataclass @@ -240,10 +263,70 @@ class RenderingLayout: """A class to describe how a work item should be rendered in a document.""" label: str | None = None - layouter: str | None = None - properties: list[dict[str, t.Any]] | None = None + layouter: Layouter | None = None + properties: RenderingProperties | None = None type: str | None = None + def __init__( + self, + label: str | None = None, + layouter: Layouter | str | None = None, + properties: list[dict[str, t.Any]] | RenderingProperties | None = None, + type: str | None = None, + ): + if isinstance(layouter, str): + layouter = Layouter(layouter) + + if isinstance(properties, list): + _properties: list[dict[str, t.Any]] = properties + properties = RenderingProperties() + for prop in _properties: + key = prop["key"] + value = prop["value"] + if key in ["fieldsAtEndAsTable", "hidden"]: + value = value == "true" + else: + value = value.split(",") + setattr(properties, RENDERING_LAYOUT_FIELDS[key], value) + + self.label = label + self.layouter = layouter + self.properties = properties + self.type = type + + +class Layouter(enum.StrEnum): + """Layout selection for work items in documents.""" + + default = "default" + paragraph = "paragraph" + section = "section" + titleTestSteps = "titleTestSteps" + titleDescTestSteps = "titleDescTestSteps" + + +@dataclasses.dataclass +class RenderingProperties: + """Properties for custom field rendering of workitems in documents.""" + + fields_at_start: list[str] | None = None + fields_at_end: list[str] | None = None + sidebar_work_item_fields: list[str] | None = None + fields_at_end_as_table: bool = False + hidden: bool = False + + def serialize(self) -> list[dict[str, t.Any]]: + """Serialize an instance of this class to be sent via the API.""" + result = [] + for pol_key, key in RENDERING_LAYOUT_FIELDS.items(): + if (value := getattr(self, key)) is not None: + if isinstance(value, list): + value = ",".join(value) + else: + value = str(value).lower() + result.append({"key": pol_key, "value": value}) + return result + @dataclasses.dataclass class TestRun(StatusItem): diff --git a/pyproject.toml b/pyproject.toml index 7d4a386e..49351bd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ no_implicit_optional = true show_error_codes = true warn_redundant_casts = true warn_unreachable = true -python_version = "3.10" +python_version = "3.11" [[tool.mypy.overrides]] module = ["tests.*"] diff --git a/tests/conftest.py b/tests/conftest.py index ce85dbcf..f792efea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -143,6 +143,7 @@ def fixture_dummy_work_item_attachment(): TEST_DOCUMENT_RESPONSE = TEST_RESPONSES / "get_document.json" TEST_DOCUMENT_POST_REQUEST = TEST_REQUESTS / "create_document.json" TEST_DOCUMENT_PATCH_REQUEST = TEST_REQUESTS / "update_document.json" +TEST_DOCUMENT_PATCH_REQUEST2 = TEST_REQUESTS / "update_document_2.json" TEST_ERROR_RESPONSE = TEST_RESPONSES / "error.json" TEST_FAULTS_ERROR_RESPONSES = TEST_RESPONSES / "faulty_errors.json" TEST_PROJECT_RESPONSE_JSON = TEST_RESPONSES / "project.json" diff --git a/tests/data/expected_requests/create_document.json b/tests/data/expected_requests/create_document.json index 51c53f32..1a12d925 100644 --- a/tests/data/expected_requests/create_document.json +++ b/tests/data/expected_requests/create_document.json @@ -1 +1 @@ -{"data": [{"type": "documents", "attributes": {"homePageContent": {"type": "text/html", "value": "super Value"}, "moduleName": "name", "title": "Fancy Title"}}]} +{"data": [{"type": "documents", "attributes": {"homePageContent": {"type": "text/html", "value": "

super Value

"}, "moduleName": "name", "outlineNumbering": {"prefix": "TEST"}, "renderingLayouts": [{"label": "My label", "layouter": "paragraph", "properties": [{"key": "fieldsAtEndAsTable", "value": "true"}, {"key": "fieldsAtStart", "value": "id"}, {"key": "fieldsAtEnd", "value": "custom"}, {"key": "sidebarWorkItemFields", "value": "id"}, {"key": "hidden", "value": "false"}], "type": "task"}, {"label": "My label", "layouter": "paragraph", "properties": [{"key": "fieldsAtEndAsTable", "value": "false"}, {"key": "fieldsAtStart", "value": "id"}, {"key": "hidden", "value": "false"}], "type": "task2"}], "title": "Fancy Title"}}]} diff --git a/tests/data/expected_requests/update_document.json b/tests/data/expected_requests/update_document.json index d1c526f0..694d1847 100644 --- a/tests/data/expected_requests/update_document.json +++ b/tests/data/expected_requests/update_document.json @@ -1 +1 @@ -{"data": {"type": "documents", "id": "PROJ/folder/name", "attributes": {"homePageContent": {"type": "text/html", "value": "super Value"}, "title": "Fancy Title"}}} +{"data": {"type": "documents", "id": "PROJ/folder/name1", "attributes": {"homePageContent": {"type": "text/html", "value": "

super Value

"}, "title": "Fancy Title"}}} diff --git a/tests/data/expected_requests/update_document_2.json b/tests/data/expected_requests/update_document_2.json new file mode 100644 index 00000000..34d4f68d --- /dev/null +++ b/tests/data/expected_requests/update_document_2.json @@ -0,0 +1 @@ +{"data": {"type": "documents", "id": "PROJ/folder/name", "attributes": {"homePageContent": {"type": "text/html", "value": "

super Value

"}, "renderingLayouts": [{"label": "My label", "layouter": "paragraph", "properties": [{"key": "fieldsAtEndAsTable", "value": "false"}, {"key": "fieldsAtStart", "value": "id"}, {"key": "fieldsAtEnd", "value": "custom"}, {"key": "sidebarWorkItemFields", "value": "id"}, {"key": "hidden", "value": "true"}], "type": "task"}, {"label": "My label", "layouter": "paragraph", "properties": [{"key": "fieldsAtEndAsTable", "value": "false"}, {"key": "fieldsAtStart", "value": "id"}, {"key": "hidden", "value": "false"}], "type": "task2"}], "title": "Fancy Title"}}} diff --git a/tests/data/expected_requests/update_document_2.json.license b/tests/data/expected_requests/update_document_2.json.license new file mode 100644 index 00000000..02c8c230 --- /dev/null +++ b/tests/data/expected_requests/update_document_2.json.license @@ -0,0 +1,2 @@ +Copyright DB InfraGO AG and contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/tests/data/mock_api_responses/get_document.json b/tests/data/mock_api_responses/get_document.json index 79221532..b6089808 100644 --- a/tests/data/mock_api_responses/get_document.json +++ b/tests/data/mock_api_responses/get_document.json @@ -11,8 +11,34 @@ }, "moduleFolder": "MySpaceId", "moduleName": "MyDocumentName", - "outlineNumbering": {}, - "renderingLayouts": [], + "outlineNumbering": { + "prefix": "PREFIX" + }, + "renderingLayouts": [ + { + "type": "task", + "label": "My label", + "layouter": "paragraph", + "properties": [ + { + "key": "fieldsAtStart", + "value": "id" + }, + { + "key": "fieldsAtEnd", + "value": "custom,bla" + }, + { + "key": "sidebarWorkItemFields", + "value": "id" + }, + { + "key": "fieldsAtEndAsTable", + "value": "true" + } + ] + } + ], "status": "open", "structureLinkRole": "parent", "title": "MyDocumentName", diff --git a/tests/test_client_documents.py b/tests/test_client_documents.py index 7187eefc..2000deae 100644 --- a/tests/test_client_documents.py +++ b/tests/test_client_documents.py @@ -10,6 +10,7 @@ import polarion_rest_api_client as polarion_api from tests.conftest import ( TEST_DOCUMENT_PATCH_REQUEST, + TEST_DOCUMENT_PATCH_REQUEST2, TEST_DOCUMENT_POST_REQUEST, TEST_DOCUMENT_RESPONSE, ) @@ -39,6 +40,22 @@ def test_get_document_with_all_fields( polarion_api.data_models.TextContent( type="text/html", value="

My text value

" ), + "MyDocumentName", + [ + polarion_api.data_models.RenderingLayout( + type="task", + label="My label", + layouter=polarion_api.data_models.Layouter("paragraph"), + properties=polarion_api.data_models.RenderingProperties( + fields_at_start=["id"], + fields_at_end=["custom", "bla"], + sidebar_work_item_fields=["id"], + fields_at_end_as_table=True, + ), + ), + ], + True, + "PREFIX", ) @@ -49,9 +66,32 @@ def test_create_new_document( module_folder="folder", module_name="name", home_page_content=polarion_api.TextContent( - type="text/html", value="super Value" + type="text/html", value="

super Value

" ), title="Fancy Title", + outline_numbering=False, + outline_numbering_prefix="TEST", + rendering_layouts=[ + polarion_api.data_models.RenderingLayout( + type="task", + label="My label", + layouter=polarion_api.data_models.Layouter("paragraph"), + properties=polarion_api.data_models.RenderingProperties( + fields_at_start=["id"], + fields_at_end=["custom"], + sidebar_work_item_fields=["id"], + fields_at_end_as_table=True, + ), + ), + polarion_api.data_models.RenderingLayout( + type="task2", + label="My label", + layouter=polarion_api.data_models.Layouter("paragraph"), + properties=polarion_api.data_models.RenderingProperties( + fields_at_start=["id"], + ), + ), + ], ) httpx_mock.add_response( @@ -86,25 +126,64 @@ def test_update_document( new_client: polarion_api.ProjectClient, httpx_mock: pytest_httpx.HTTPXMock ): document = polarion_api.Document( + module_folder="folder", + module_name="name1", + home_page_content=polarion_api.TextContent( + type="text/html", value="

super Value

" + ), + title="Fancy Title", + ) + + document2 = polarion_api.Document( module_folder="folder", module_name="name", home_page_content=polarion_api.TextContent( - type="text/html", value="super Value" + type="text/html", value="

super Value

" ), title="Fancy Title", + rendering_layouts=[ + polarion_api.data_models.RenderingLayout( + type="task", + label="My label", + layouter=polarion_api.data_models.Layouter("paragraph"), + properties=polarion_api.data_models.RenderingProperties( + fields_at_start=["id"], + fields_at_end=["custom"], + sidebar_work_item_fields=["id"], + hidden=True, + ), + ), + polarion_api.data_models.RenderingLayout( + type="task2", + label="My label", + layouter=polarion_api.data_models.Layouter("paragraph"), + properties=polarion_api.data_models.RenderingProperties( + fields_at_start=["id"], + ), + ), + ], ) httpx_mock.add_response(204) - new_client.documents.update(document) + httpx_mock.add_response(204) + new_client.documents.update([document, document2]) with open(TEST_DOCUMENT_PATCH_REQUEST, "r", encoding="utf-8") as f: expected_request = json.load(f) + with open(TEST_DOCUMENT_PATCH_REQUEST2, "r", encoding="utf-8") as f: + expected_request_2 = json.load(f) + reqs = httpx_mock.get_requests() - assert len(httpx_mock.get_requests()) == 1 - req = httpx_mock.get_request() - assert req.method == "PATCH" + assert len(reqs) == 2 + assert reqs[0].method == "PATCH" assert ( - req.url + reqs[0].url + == "http://127.0.0.1/api/projects/PROJ/spaces/folder/documents/name1" + ) + assert json.loads(reqs[0].content.decode("utf-8")) == expected_request + assert reqs[1].method == "PATCH" + assert ( + reqs[1].url == "http://127.0.0.1/api/projects/PROJ/spaces/folder/documents/name" ) - assert json.loads(req.content.decode("utf-8")) == expected_request + assert json.loads(reqs[1].content.decode("utf-8")) == expected_request_2 From 588d6e16edebcca03bcbba45069b5f48894b96ca Mon Sep 17 00:00:00 2001 From: Michael Harbarth Date: Tue, 23 Jul 2024 14:21:06 +0200 Subject: [PATCH 2/4] feat: Adjust the Layouter enum and use the regular Enum to support python 3.10 --- polarion_rest_api_client/clients/documents.py | 12 ++++++++++-- polarion_rest_api_client/data_models.py | 13 +++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/polarion_rest_api_client/clients/documents.py b/polarion_rest_api_client/clients/documents.py index b0916a3b..a0a0079c 100644 --- a/polarion_rest_api_client/clients/documents.py +++ b/polarion_rest_api_client/clients/documents.py @@ -144,7 +144,11 @@ def _update(self, to_update: dm.Document | list[dm.Document]): [ api_models.DocumentsSinglePatchRequestDataAttributesRenderingLayoutsItem( label=layout.label or oa_types.UNSET, - layouter=layout.layouter or oa_types.UNSET, + layouter=( + layout.layouter.value + if layout.layouter is not None + else oa_types.UNSET + ), type=layout.type or oa_types.UNSET, properties=( [ @@ -221,7 +225,11 @@ def _create(self, items: list[dm.Document]): [ api_models.DocumentsListPostRequestDataItemAttributesRenderingLayoutsItem( label=layout.label or oa_types.UNSET, - layouter=layout.layouter or oa_types.UNSET, + layouter=( + layout.layouter.value + if layout.layouter is not None + else oa_types.UNSET + ), type=layout.type or oa_types.UNSET, properties=( [ diff --git a/polarion_rest_api_client/data_models.py b/polarion_rest_api_client/data_models.py index 58ff2c09..45223d5b 100644 --- a/polarion_rest_api_client/data_models.py +++ b/polarion_rest_api_client/data_models.py @@ -295,14 +295,15 @@ def __init__( self.type = type -class Layouter(enum.StrEnum): +class Layouter(enum.Enum): """Layout selection for work items in documents.""" - default = "default" - paragraph = "paragraph" - section = "section" - titleTestSteps = "titleTestSteps" - titleDescTestSteps = "titleDescTestSteps" + DEFAULT = "default" # Seems to be title only + TITLE = "title" # Title only + PARAGRAPH = "paragraph" # Description only + SECTION = "section" # Title and description + TITLE_TEST_STEPS = "titleTestSteps" # Title and testSteps + TITLE_DESC_TEST_STEPS = "titleDescTestSteps" # Title,description,testSteps @dataclasses.dataclass From 1ba7916d4625a8df59cc073e0235dd5ffcffed4d Mon Sep 17 00:00:00 2001 From: Michael Harbarth Date: Tue, 23 Jul 2024 19:04:02 +0200 Subject: [PATCH 3/4] fix: support missing value fields in rendering propterties --- polarion_rest_api_client/data_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polarion_rest_api_client/data_models.py b/polarion_rest_api_client/data_models.py index 45223d5b..3f082093 100644 --- a/polarion_rest_api_client/data_models.py +++ b/polarion_rest_api_client/data_models.py @@ -282,7 +282,7 @@ def __init__( properties = RenderingProperties() for prop in _properties: key = prop["key"] - value = prop["value"] + value = prop.get("value", "") if key in ["fieldsAtEndAsTable", "hidden"]: value = value == "true" else: From 0e07b4123166b2dcac3fa56e5678b39da9979167 Mon Sep 17 00:00:00 2001 From: Michael Harbarth Date: Thu, 25 Jul 2024 18:22:19 +0200 Subject: [PATCH 4/4] refactor: implement requested changes from review --- polarion_rest_api_client/data_models.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/polarion_rest_api_client/data_models.py b/polarion_rest_api_client/data_models.py index 3f082093..3a87416c 100644 --- a/polarion_rest_api_client/data_models.py +++ b/polarion_rest_api_client/data_models.py @@ -11,11 +11,7 @@ import json import typing as t -field_at_start: list[str] | None = None -fields_at_end: list[str] | None = None -sidebar_work_item_fields: list[str] | None = None -fields_at_end_as_table: bool = False - +BOOLEAN_RENDERING_PROPERTIES = ["fieldsAtEndAsTable", "hidden"] RENDERING_LAYOUT_FIELDS = { "fieldsAtEndAsTable": "fields_at_end_as_table", "fieldsAtStart": "fields_at_start", @@ -283,7 +279,7 @@ def __init__( for prop in _properties: key = prop["key"] value = prop.get("value", "") - if key in ["fieldsAtEndAsTable", "hidden"]: + if key in BOOLEAN_RENDERING_PROPERTIES: value = value == "true" else: value = value.split(",")