diff --git a/capella2polarion/__main__.py b/capella2polarion/__main__.py index 77a136d7..a9e93ea6 100644 --- a/capella2polarion/__main__.py +++ b/capella2polarion/__main__.py @@ -167,13 +167,10 @@ def render_documents( overwrite_layouts, ) - new_documents, updated_documents, work_items = renderer.render_documents( - configs, documents - ) - - polarion_worker.post_documents(new_documents) - polarion_worker.update_documents(updated_documents) - polarion_worker.update_work_items(work_items) + projects_document_data = renderer.render_documents(configs, documents) + for project, project_data in projects_document_data.items(): + polarion_worker.create_documents(project_data.new_docs, project) + polarion_worker.update_documents(project_data.updated_docs, project) if __name__ == "__main__": diff --git a/capella2polarion/connectors/polarion_repo.py b/capella2polarion/connectors/polarion_repo.py index 7acd47d0..f40f70ca 100644 --- a/capella2polarion/connectors/polarion_repo.py +++ b/capella2polarion/connectors/polarion_repo.py @@ -6,6 +6,7 @@ import collections.abc as cabc import bidict +import polarion_rest_api_client as polarion_api from capella2polarion import data_models @@ -114,3 +115,15 @@ def remove_work_items_by_capella_uuid(self, uuids: cabc.Iterable[str]): for uuid in uuids: del self._work_items[uuid] del self._id_mapping[uuid] + + +DocumentRepository = dict[ + tuple[str | None, str, str], + tuple[polarion_api.Document | None, list[polarion_api.WorkItem]], +] +"""A dict providing a mapping for documents and their text workitems. + +It has (project, space, name) of the document as key and (document, +workitems) as value. The project can be None and the None value means +that the document is in the same project as the model sync work items. +""" diff --git a/capella2polarion/connectors/polarion_worker.py b/capella2polarion/connectors/polarion_worker.py index b60ae11d..e6b22371 100644 --- a/capella2polarion/connectors/polarion_worker.py +++ b/capella2polarion/connectors/polarion_worker.py @@ -25,6 +25,15 @@ int: 0, bool: False, } +WORK_ITEMS_IN_PROJECT_QUERY = ( + "SQL:(SELECT item.* FROM POLARION.WORKITEM item, POLARION.MODULE doc, " + "POLARION.PROJECT proj WHERE proj.C_ID = '{project}' AND " + "doc.FK_PROJECT = proj.C_PK AND doc.C_ID = '{doc_name}' AND " + "doc.C_MODULEFOLDER = '{doc_folder}' AND item.C_TYPE = '{wi_type}' AND " + "EXISTS (SELECT rel1.* FROM POLARION.REL_MODULE_WORKITEM rel1 WHERE " + "rel1.FK_URI_MODULE = doc.C_URI AND rel1.FK_URI_WORKITEM = item.C_URI))" +) +"""An SQL query to get work items which are inserted in a given document.""" class PolarionWorkerParams: @@ -68,19 +77,43 @@ def __init__( "Polarion PAT (Personal Access Token) parameter " "is not a set properly." ) - self.client = polarion_api.OpenAPIPolarionProjectClient( - self.polarion_params.project_id, - self.polarion_params.delete_work_items, + + self.polarion_client = polarion_api.PolarionClient( polarion_api_endpoint=f"{self.polarion_params.url}/rest/v1", polarion_access_token=self.polarion_params.private_access_token, - custom_work_item=data_models.CapellaWorkItem, + ) + self.project_client = self.polarion_client.generate_project_client( + project_id=self.polarion_params.project_id, + delete_status=( + "deleted" if self.polarion_params.delete_work_items else None + ), add_work_item_checksum=True, ) + self._additional_clients: dict[str, polarion_api.ProjectClient] = {} self.check_client() + def _get_client( + self, project_id: str | None + ) -> polarion_api.ProjectClient: + if project_id is None: + return self.project_client + if project_id in self._additional_clients: + return self._additional_clients[project_id] + client = self.polarion_client.generate_project_client( + project_id=project_id, + delete_status=( + "deleted" if self.polarion_params.delete_work_items else None + ), + add_work_item_checksum=True, + ) + if not client.exists(): + raise KeyError(f"Miss Polarion project with id {project_id}") + self._additional_clients[project_id] = client + return client + def check_client(self) -> None: """Instantiate the polarion client as member.""" - if not self.client.project_exists(): + if not self.project_client.exists(): raise KeyError( "Miss Polarion project with id " f"{self.polarion_params.project_id}" @@ -88,9 +121,10 @@ def check_client(self) -> None: def load_polarion_work_item_map(self): """Return a map from Capella UUIDs to Polarion work items.""" - work_items = self.client.get_all_work_items( + work_items = self.project_client.work_items.get_all( "HAS_VALUE:uuid_capella", - {"workitems": "id,uuid_capella,checksum,status,type"}, + fields={"workitems": "id,uuid_capella,checksum,status,type"}, + work_item_cls=data_models.CapellaWorkItem, ) self.polarion_data_repo.update_work_items(work_items) @@ -103,28 +137,27 @@ def delete_orphaned_work_items( are marked as ``to be deleted`` via the status attribute. """ - def serialize_for_delete(uuid: str) -> str: - work_item_id, _ = self.polarion_data_repo[uuid] - logger.info("Delete work item %r...", work_item_id) - return work_item_id - existing_work_items = { uuid for uuid, _, work_item in self.polarion_data_repo.items() if work_item.status != "deleted" } uuids: set[str] = existing_work_items - set(converter_session) - work_item_ids = [serialize_for_delete(uuid) for uuid in uuids] - if work_item_ids: + work_items: list[data_models.CapellaWorkItem] = [] + for uuid in uuids: + if wi := self.polarion_data_repo.get_work_item_by_capella_uuid( + uuid + ): + logger.info("Delete work item %r...", wi.id) + work_items.append(wi) try: - self.client.delete_work_items(work_item_ids) - self.polarion_data_repo.remove_work_items_by_capella_uuid( - uuids - ) + self.project_client.work_items.delete(work_items) except polarion_api.PolarionApiException as error: logger.error("Deleting work items failed. %s", error.args[0]) raise error + self.polarion_data_repo.remove_work_items_by_capella_uuid(uuids) + def create_missing_work_items( self, converter_session: data_session.ConverterSession ) -> None: @@ -146,7 +179,7 @@ def create_missing_work_items( logger.info("Create work item for %r...", work_item.title) if missing_work_items: try: - self.client.create_work_items(missing_work_items) + self.project_client.work_items.create(missing_work_items) self.polarion_data_repo.update_work_items(missing_work_items) except polarion_api.PolarionApiException as error: logger.error("Creating work items failed. %s", error.args[0]) @@ -184,18 +217,22 @@ def compare_and_update_work_item( work_item_changed = new_work_item_check_sum != old_work_item_check_sum try: if work_item_changed or self.force_update: - old = self.client.get_work_item(old.id) + old = self.project_client.work_items.get( + old.id, work_item_cls=data_models.CapellaWorkItem + ) if old.attachments: old_attachments = ( - self.client.get_all_work_item_attachments( + self.project_client.work_items.attachments.get_all( work_item_id=old.id ) ) else: old_attachments = [] else: - old_attachments = self.client.get_all_work_item_attachments( - work_item_id=old.id + old_attachments = ( + self.project_client.work_items.attachments.get_all( + work_item_id=old.id + ) ) if old_attachments or new.attachments: work_item_changed |= self.update_attachments( @@ -221,8 +258,8 @@ def compare_and_update_work_item( del old.additional_attributes["uuid_capella"] if old.linked_work_items_truncated: - old.linked_work_items = self.client.get_all_work_item_links( - old.id + old.linked_work_items = ( + self.project_client.work_items.links.get_all(old.id) ) # Type will only be updated, if set and should be used carefully @@ -256,7 +293,7 @@ def compare_and_update_work_item( new.title = None try: - self.client.update_work_item(new) + self.project_client.work_items.update(new) if delete_links: id_list_str = ", ".join(delete_links.keys()) logger.info( @@ -265,7 +302,9 @@ def compare_and_update_work_item( new.type, new.title, ) - self.client.delete_work_item_links(list(delete_links.values())) + self.project_client.work_items.links.delete( + list(delete_links.values()) + ) if create_links: id_list_str = ", ".join(create_links.keys()) @@ -275,7 +314,9 @@ def compare_and_update_work_item( new.type, new.title, ) - self.client.create_work_item_links(list(create_links.values())) + self.project_client.work_items.links.create( + list(create_links.values()) + ) except polarion_api.PolarionApiException as error: logger.error( @@ -346,12 +387,12 @@ def update_attachments( attachment.file_name, attachment.id, ) - self.client.delete_work_item_attachment(attachment) + self.project_client.work_items.attachments.delete(attachment) old_attachment_file_names = set(old_attachment_dict) new_attachment_file_names = set(new_attachment_dict) for file_name in old_attachment_file_names - new_attachment_file_names: - self.client.delete_work_item_attachment( + self.project_client.work_items.attachments.delete( old_attachment_dict[file_name] ) @@ -361,7 +402,7 @@ def update_attachments( new_attachment_file_names - old_attachment_file_names, ) ): - self.client.create_work_item_attachments(new_attachments) + self.project_client.work_items.attachments.create(new_attachments) created = True attachments_for_update = {} @@ -386,7 +427,7 @@ def update_attachments( ): continue - self.client.update_work_item_attachment(attachment) + self.project_client.work_items.attachments.update(attachment) return created @staticmethod @@ -425,20 +466,76 @@ def compare_and_update_work_items( if uuid in self.polarion_data_repo and data.work_item is not None: self.compare_and_update_work_item(data) - def post_documents(self, documents: list[polarion_api.Document]): - """Create new documents.""" - self.client.project_client.documents.create(documents) + def create_documents( + self, + document_datas: list[data_models.DocumentData], + document_project: str | None = None, + ): + """Create new documents. + + Notes + ----- + If the ``document_project`` is ``None`` the default client is + taken. + """ + client = self._get_client(document_project) + documents, _ = self._process_document_datas(client, document_datas) + + client.documents.create(documents) - def update_documents(self, documents: list[polarion_api.Document]): - """Update existing documents.""" - self.client.project_client.documents.update(documents) + def update_documents( + self, + document_datas: list[data_models.DocumentData], + document_project: str | None = None, + ): + """Update existing documents. + + Notes + ----- + If the ``document_project`` is ``None`` the default client is + taken. + """ + client = self._get_client(document_project) + documents, headings = self._process_document_datas( + client, document_datas + ) + + client.work_items.update(headings) + client.documents.update(documents) + + def _process_document_datas( + self, + client: polarion_api.ProjectClient, + document_datas: list[data_models.DocumentData], + ): + documents: list[polarion_api.Document] = [] + headings: list[polarion_api.WorkItem] = [] + for document_data in document_datas: + headings.extend(document_data.headings) + documents.append(document_data.document) + if document_data.text_work_item_provider.new_text_work_items: + self._create_and_update_text_work_items( + document_data.text_work_item_provider.new_text_work_items, + client, + ) + document_data.text_work_item_provider.insert_text_work_items( + document_data.document, + ) + return documents, headings def get_document( - self, space: str, name: str + self, space: str, name: str, document_project: str | None = None ) -> polarion_api.Document | None: - """Get a document from polarion and return None if not found.""" + """Get a document from polarion and return None if not found. + + Notes + ----- + If the ``document_project`` is ``None`` the default client is + taken. + """ + client = self._get_client(document_project) try: - return self.client.project_client.documents.get( + return client.documents.get( space, name, fields={"documents": "@all"} ) except polarion_api.PolarionApiBaseException as e: @@ -446,15 +543,42 @@ def get_document( return None raise e - def update_work_items(self, work_items: list[polarion_api.WorkItem]): - """Update the given workitems without any additional checks.""" - self.client.project_client.work_items.update(work_items) - def load_polarion_documents( - self, document_paths: t.Iterable[tuple[str, str]] - ) -> dict[tuple[str, str], polarion_api.Document | None]: - """Load the given document references from Polarion.""" + self, + document_infos: t.Iterable[data_models.DocumentInfo], + ) -> polarion_repo.DocumentRepository: + """Load the documents referenced and text work items from Polarion.""" return { - (space, name): self.get_document(space, name) - for space, name in document_paths + (di.project_id, di.module_folder, di.module_name): ( + self.get_document( + di.module_folder, di.module_name, di.project_id + ), + self._get_client(di.project_id).work_items.get_all( + WORK_ITEMS_IN_PROJECT_QUERY.format( + project=di.project_id + or self.polarion_params.project_id, + doc_folder=di.module_folder, + doc_name=di.module_name, + wi_type=di.text_work_item_type, + ), + fields={"workitems": f"id,{di.text_work_item_id_field}"}, + ), + ) + for di in document_infos } + + def _create_and_update_text_work_items( + self, + work_items: dict[str, polarion_api.WorkItem], + client: polarion_api.ProjectClient, + ): + client.work_items.update( + [work_item for work_item in work_items.values() if work_item.id] + ) + client.work_items.create( + [ + work_item + for work_item in work_items.values() + if not work_item.id + ] + ) diff --git a/capella2polarion/converters/document_config.py b/capella2polarion/converters/document_config.py index 30ab8df6..6a936666 100644 --- a/capella2polarion/converters/document_config.py +++ b/capella2polarion/converters/document_config.py @@ -11,6 +11,7 @@ import pydantic import yaml +from capella2polarion import data_models from capella2polarion.converters import polarion_html_helper logger = logging.getLogger(__name__) @@ -47,6 +48,10 @@ class BaseDocumentRenderingConfig(pydantic.BaseModel): """A template config, which can result in multiple Polarion documents.""" template_directory: str | pathlib.Path + project_id: str | None = None + text_work_item_type: str = polarion_html_helper.TEXT_WORK_ITEM_TYPE + text_work_item_id_field: str = polarion_html_helper.TEXT_WORK_ITEM_ID_FIELD + status_allow_list: list[str] | None = None heading_numbering: bool = False work_item_layouts: dict[str, WorkItemLayout] = pydantic.Field( default_factory=dict @@ -77,11 +82,17 @@ class DocumentConfigs(pydantic.BaseModel): pydantic.Field(default_factory=list) ) - def iterate_documents(self) -> t.Iterator[tuple[str, str]]: + def iterate_documents(self) -> t.Iterator[data_models.DocumentInfo]: """Yield all document paths of the config as tuples.""" for conf in self.full_authority + self.mixed_authority: for inst in conf.instances: - yield inst.polarion_space, inst.polarion_name + yield data_models.DocumentInfo( + project_id=conf.project_id, + module_folder=inst.polarion_space, + module_name=inst.polarion_name, + text_work_item_type=conf.text_work_item_type, + text_work_item_id_field=conf.text_work_item_id_field, + ) def read_config_file( diff --git a/capella2polarion/converters/document_renderer.py b/capella2polarion/converters/document_renderer.py index 7b012754..58e7af59 100644 --- a/capella2polarion/converters/document_renderer.py +++ b/capella2polarion/converters/document_renderer.py @@ -16,7 +16,9 @@ from capella2polarion.connectors import polarion_repo +from .. import data_models from . import document_config, polarion_html_helper +from . import text_work_item_provider as twi logger = logging.getLogger(__name__) @@ -35,6 +37,21 @@ class RenderingSession: inserted_work_items: list[polarion_api.WorkItem] = dataclasses.field( default_factory=list ) + text_work_items: dict[str, polarion_api.WorkItem] = dataclasses.field( + default_factory=dict + ) + + +@dataclasses.dataclass +class ProjectData: + """A class holding data of a project which documents are rendered for.""" + + new_docs: list[data_models.DocumentData] = dataclasses.field( + default_factory=list + ) + updated_docs: list[data_models.DocumentData] = dataclasses.field( + default_factory=list + ) class DocumentRenderer(polarion_html_helper.JinjaRendererMixin): @@ -52,6 +69,8 @@ def __init__( self.jinja_envs: dict[str, jinja2.Environment] = {} self.overwrite_heading_numbering = overwrite_heading_numbering self.overwrite_layouts = overwrite_layouts + self.projects: dict[str | None, ProjectData] = {} + self.existing_documents: polarion_repo.DocumentRepository = {} def setup_env(self, env: jinja2.Environment): """Add globals and filters to the environment.""" @@ -79,22 +98,10 @@ def __insert_work_item( ) return f"

{self.__link_work_item(obj)}

" - layout_index = 0 - for layout in session.rendering_layouts: - if layout.type == wi.type: - break - layout_index += 1 - - if layout_index >= len(session.rendering_layouts): - session.rendering_layouts.append( - polarion_api.RenderingLayout( - type=wi.type, - layouter="section", - label=polarion_html_helper.camel_case_to_words( - wi.type - ), - ) - ) + assert wi.type + layout_index = polarion_html_helper.get_layout_index( + "section", session.rendering_layouts, wi.type + ) custom_info = "" if level is not None: @@ -131,7 +138,7 @@ def __heading(self, level: int, text: str, session: RenderingSession): session.headings.append(polarion_api.WorkItem(id=hid, title=text)) return ( f"' + f'id="{polarion_html_helper.wi_id_prefix}{hid}">' f"" ) return f"{text}" @@ -159,8 +166,10 @@ def render_document( document_title: str | None = None, heading_numbering: bool = False, rendering_layouts: list[polarion_api.RenderingLayout] | None = None, + *, + text_work_item_provider: twi.TextWorkItemProvider | None = None, **kwargs: t.Any, - ): + ) -> data_models.DocumentData: """Render a new Polarion document.""" @t.overload @@ -170,8 +179,9 @@ def render_document( template_name: str, *, document: polarion_api.Document, + text_work_item_provider: twi.TextWorkItemProvider | None = None, **kwargs: t.Any, - ): + ) -> data_models.DocumentData: """Update an existing Polarion document.""" def render_document( @@ -184,9 +194,13 @@ def render_document( heading_numbering: bool = False, rendering_layouts: list[polarion_api.RenderingLayout] | None = None, document: polarion_api.Document | None = None, + text_work_item_provider: twi.TextWorkItemProvider | None = None, **kwargs: t.Any, - ): + ) -> data_models.DocumentData: """Render a Polarion document.""" + text_work_item_provider = ( + text_work_item_provider or twi.TextWorkItemProvider() + ) if document is not None: polarion_folder = document.module_folder polarion_name = document.module_name @@ -216,13 +230,22 @@ def render_document( if rendering_layouts is not None: session.rendering_layouts = rendering_layouts + rendering_result = template.render( + model=self.model, session=session, **kwargs + ) + text_work_item_provider.generate_text_work_items( + lxmlhtml.fragments_fromstring(rendering_result), + ) + document.home_page_content = polarion_api.TextContent( "text/html", - template.render(model=self.model, session=session, **kwargs), + rendering_result, ) document.rendering_layouts = session.rendering_layouts - return document, session.headings + return data_models.DocumentData( + document, session.headings, text_work_item_provider + ) def update_mixed_authority_document( self, @@ -231,8 +254,12 @@ def update_mixed_authority_document( sections: dict[str, str], global_parameters: dict[str, t.Any], section_parameters: dict[str, dict[str, t.Any]], - ): + text_work_item_provider: twi.TextWorkItemProvider | None = None, + ) -> data_models.DocumentData: """Update a mixed authority document.""" + text_work_item_provider = ( + text_work_item_provider or twi.TextWorkItemProvider() + ) assert ( document.home_page_content and document.home_page_content.value ), "In mixed authority the document must have content" @@ -272,7 +299,14 @@ def update_mixed_authority_document( | section_parameters.get(section_name, {}) ), ) - new_content += lxmlhtml.fragments_fromstring(content) + work_item_ids = polarion_html_helper.extract_work_items( + current_content + ) + html_fragments = lxmlhtml.fragments_fromstring(content) + text_work_item_provider.generate_text_work_items( + html_fragments, work_item_ids + ) + new_content += html_fragments new_content += html_elements[last_section_end:] new_content = polarion_html_helper.remove_table_ids(new_content) @@ -288,20 +322,23 @@ def update_mixed_authority_document( ) document.rendering_layouts = session.rendering_layouts - return document, session.headings + return data_models.DocumentData( + document, session.headings, text_work_item_provider + ) def _get_and_customize_doc( self, + project_id: str | None, space: str, name: str, title: str | None, rendering_layouts: list[polarion_api.RenderingLayout], heading_numbering: bool, - existing_documents: dict[ - tuple[str, str], polarion_api.Document | None - ], - ) -> polarion_api.Document | None: - if old_doc := existing_documents.get((space, name)): + ) -> tuple[polarion_api.Document | None, list[polarion_api.WorkItem]]: + old_doc, text_work_items = self.existing_documents.get( + (project_id, space, name), (None, []) + ) + if old_doc: if title: old_doc.title = title if self.overwrite_layouts: @@ -309,68 +346,72 @@ def _get_and_customize_doc( if self.overwrite_heading_numbering: old_doc.outline_numbering = heading_numbering - return old_doc + return old_doc, text_work_items def render_documents( self, configs: document_config.DocumentConfigs, - existing_documents: dict[ - tuple[str, str], polarion_api.Document | None - ], - ) -> tuple[ - list[polarion_api.Document], - list[polarion_api.Document], - list[polarion_api.WorkItem], - ]: + existing_documents: polarion_repo.DocumentRepository, + ) -> dict[str | None, ProjectData]: """Render all documents defined in the given config. Returns a list new documents followed by updated documents and work items, which need to be updated """ + self.existing_documents = existing_documents + self.projects = {} - new_docs: list[polarion_api.Document] = [] - updated_docs: list[polarion_api.Document] = [] - work_items: list[polarion_api.WorkItem] = [] - self._render_full_authority_documents( - configs.full_authority, - existing_documents, - new_docs, - updated_docs, - work_items, - ) + self._render_full_authority_documents(configs.full_authority) + self._render_mixed_authority_documents(configs.mixed_authority) - self._render_mixed_authority_documents( - configs.mixed_authority, - existing_documents, - updated_docs, - work_items, - ) + return self.projects - return new_docs, updated_docs, work_items + def _check_document_status( + self, + document: polarion_api.Document, + config: document_config.BaseDocumentRenderingConfig, + ): + if ( + config.status_allow_list is not None + and document.status not in config.status_allow_list + ): + logger.warning( + "Won't update document %s/%s due to status " + "restrictions. Status is %s and should be in %r.", + document.module_folder, + document.module_name, + document.status, + config.status_allow_list, + ) + return False + return True def _render_mixed_authority_documents( self, mixed_authority_configs: list[ - document_config.FullAuthorityDocumentRenderingConfig + document_config.MixedAuthorityDocumentRenderingConfig ], - existing_documents: dict[ - tuple[str, str], polarion_api.Document | None - ], - updated_docs: list[polarion_api.Document], - work_items: list[polarion_api.WorkItem], ): for config in mixed_authority_configs: rendering_layouts = document_config.generate_work_item_layouts( config.work_item_layouts ) + project_data = self.projects.setdefault( + config.project_id, ProjectData() + ) for instance in config.instances: - old_doc = self._get_and_customize_doc( + old_doc, text_work_items = self._get_and_customize_doc( + config.project_id, instance.polarion_space, instance.polarion_name, instance.polarion_title, rendering_layouts, config.heading_numbering, - existing_documents, + ) + text_work_item_provider = twi.TextWorkItemProvider( + config.text_work_item_id_field, + config.text_work_item_type, + text_work_items, ) if old_doc is None: logger.error( @@ -380,13 +421,18 @@ def _render_mixed_authority_documents( instance.polarion_name, ) continue + + if not self._check_document_status(old_doc, config): + continue + try: - new_doc, wis = self.update_mixed_authority_document( + document_data = self.update_mixed_authority_document( old_doc, config.template_directory, config.sections, instance.params, instance.section_params, + text_work_item_provider, ) except Exception as e: logger.error( @@ -398,37 +444,43 @@ def _render_mixed_authority_documents( ) continue - updated_docs.append(new_doc) - work_items.extend(wis) + project_data.updated_docs.append(document_data) def _render_full_authority_documents( self, full_authority_configs, - existing_documents: dict[ - tuple[str, str], polarion_api.Document | None - ], - new_docs: list[polarion_api.Document], - updated_docs: list[polarion_api.Document], - work_items: list[polarion_api.WorkItem], ): for config in full_authority_configs: rendering_layouts = document_config.generate_work_item_layouts( config.work_item_layouts ) + project_data = self.projects.setdefault( + config.project_id, ProjectData() + ) for instance in config.instances: - if old_doc := self._get_and_customize_doc( + old_doc, text_work_items = self._get_and_customize_doc( + config.project_id, instance.polarion_space, instance.polarion_name, instance.polarion_title, rendering_layouts, config.heading_numbering, - existing_documents, - ): + ) + text_work_item_provider = twi.TextWorkItemProvider( + config.text_work_item_id_field, + config.text_work_item_type, + text_work_items, + ) + if old_doc: + if not self._check_document_status(old_doc, config): + continue + try: - new_doc, wis = self.render_document( + document_data = self.render_document( config.template_directory, config.template, document=old_doc, + text_work_item_provider=text_work_item_provider, **instance.params, ) except Exception as e: @@ -441,11 +493,10 @@ def _render_full_authority_documents( ) continue - updated_docs.append(new_doc) - work_items.extend(wis) + project_data.updated_docs.append(document_data) else: try: - new_doc, _ = self.render_document( + document_data = self.render_document( config.template_directory, config.template, instance.polarion_space, @@ -453,6 +504,7 @@ def _render_full_authority_documents( instance.polarion_title, config.heading_numbering, rendering_layouts, + text_work_item_provider=text_work_item_provider, **instance.params, ) except Exception as e: @@ -465,7 +517,7 @@ def _render_full_authority_documents( ) continue - new_docs.append(new_doc) + project_data.new_docs.append(document_data) def _extract_section_areas(self, html_elements: list[etree._Element]): section_areas = {} diff --git a/capella2polarion/converters/link_converter.py b/capella2polarion/converters/link_converter.py index bf8bb511..a618b087 100644 --- a/capella2polarion/converters/link_converter.py +++ b/capella2polarion/converters/link_converter.py @@ -13,10 +13,13 @@ from capellambse.model import common from capellambse.model import diagram as diag -import capella2polarion.converters.polarion_html_helper from capella2polarion import data_models from capella2polarion.connectors import polarion_repo -from capella2polarion.converters import converter_config, data_session +from capella2polarion.converters import ( + converter_config, + data_session, + polarion_html_helper, +) logger = logging.getLogger(__name__) @@ -346,9 +349,7 @@ def _group_by( def _make_url_list(link_map: dict[str, dict[str, list[str]]]) -> str: urls: list[str] = [] for link_id in sorted(link_map): - url = capella2polarion.converters.polarion_html_helper.POLARION_WORK_ITEM_URL.format( # pylint: disable=line-too-long - pid=link_id - ) + url = polarion_html_helper.POLARION_WORK_ITEM_URL.format(pid=link_id) urls.append(f"
  • {url}
  • ") for key, include_wids in link_map[link_id].items(): _, display_name, _ = key.split(":") @@ -365,9 +366,7 @@ def _sorted_unordered_html_list( ) -> str: urls: list[str] = [] for pid in work_item_ids: - url = capella2polarion.converters.polarion_html_helper.POLARION_WORK_ITEM_URL.format( # pylint: disable=line-too-long - pid=pid - ) + url = polarion_html_helper.POLARION_WORK_ITEM_URL.format(pid=pid) urls.append(f"
  • {url}
  • ") urls.sort() diff --git a/capella2polarion/converters/polarion_html_helper.py b/capella2polarion/converters/polarion_html_helper.py index 9d7fa573..2cc20ac7 100644 --- a/capella2polarion/converters/polarion_html_helper.py +++ b/capella2polarion/converters/polarion_html_helper.py @@ -8,14 +8,16 @@ import capellambse import jinja2 +import polarion_rest_api_client as polarion_api from capellambse import helpers as chelpers -from lxml import etree, html +from lxml import html -heading_id_prefix = "polarion_wiki macro name=module-workitem;params=id=" +wi_id_prefix = "polarion_wiki macro name=module-workitem;params=id=" h_regex = re.compile("h[0-9]") -wi_regex = re.compile(f"{heading_id_prefix}(.*)") - +wi_id_regex = re.compile(f"{wi_id_prefix}([A-Z|a-z|0-9]*-[0-9]+)") +TEXT_WORK_ITEM_ID_FIELD = "__C2P__id" +TEXT_WORK_ITEM_TYPE = "text" POLARION_WORK_ITEM_URL = ( '' @@ -29,6 +31,7 @@ f"<deleted element ({chelpers.RE_VALID_UUID.pattern})>" ) RED_TEXT = '

    {text}

    ' +WORK_ITEM_TAG = "workitem" def strike_through(string: str) -> str: @@ -111,8 +114,8 @@ def setup_env(self, env: jinja2.Environment): def remove_table_ids( - html_content: str | list[etree._Element], -) -> list[etree._Element]: + html_content: str | list[html.HtmlElement | str], +) -> list[html.HtmlElement | str]: """Remove the ID field from all tables. This is necessary due to a bug in Polarion where Polarion does not @@ -120,34 +123,73 @@ def remove_table_ids( time the REST-API does not allow posting or patching a document with multiple tables having the same ID. """ - html_fragments = _ensure_fragments(html_content) + html_fragments = ensure_fragments(html_content) for element in html_fragments: + if not isinstance(element, html.HtmlElement): + continue + if element.tag == "table": - element.remove("id") + element.attrib.pop("id", None) return html_fragments -def _ensure_fragments( - html_content: str | list[etree._Element], -) -> list[etree._Element]: +def ensure_fragments( + html_content: str | list[html.HtmlElement | str], +) -> list[html.HtmlElement | str]: + """Convert string to html elements.""" if isinstance(html_content, str): return html.fragments_fromstring(html_content) return html_content -def extract_headings(html_content: str | list[etree._Element]) -> list[str]: +def extract_headings( + html_content: str | list[html.HtmlElement | str], +) -> list[str]: """Return a list of work item IDs for all headings in the given content.""" - heading_ids = [] - html_fragments = _ensure_fragments(html_content) + return extract_work_items(html_content, h_regex) + +def extract_work_items( + html_content: str | list[html.HtmlElement | str], + tag_regex: re.Pattern | None = None, +) -> list[str]: + """Return a list of work item IDs for work items in the given content.""" + work_item_ids: list[str] = [] + html_fragments = ensure_fragments(html_content) for element in html_fragments: - if isinstance(element, html.HtmlComment): + if not isinstance(element, html.HtmlElement): continue - if h_regex.fullmatch(element.tag): - if matches := wi_regex.match(element.get("id")): - heading_ids.append(matches.group(1)) + if (tag_regex is not None and tag_regex.fullmatch(element.tag)) or ( + tag_regex is None and element.tag == "div" + ): + if matches := wi_id_regex.match(element.get("id")): + work_item_ids.append(matches.group(1)) + return work_item_ids - return heading_ids + +def get_layout_index( + default_layouter: str, + rendering_layouts: list[polarion_api.RenderingLayout], + work_item_type: str, +) -> int: + """Return the index of the layout of the requested workitem. + + If there is no rendering config yet, it will be created. + """ + layout_index = 0 + for layout in rendering_layouts: + if layout.type == work_item_type: + return layout_index + layout_index += 1 + if layout_index >= len(rendering_layouts): + rendering_layouts.append( + polarion_api.RenderingLayout( + type=work_item_type, + layouter=default_layouter, + label=camel_case_to_words(work_item_type), + ) + ) + return layout_index diff --git a/capella2polarion/converters/text_work_item_provider.py b/capella2polarion/converters/text_work_item_provider.py new file mode 100644 index 00000000..a75e022a --- /dev/null +++ b/capella2polarion/converters/text_work_item_provider.py @@ -0,0 +1,125 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 +"""Provides a class to generate and inset text work items in documents.""" +import polarion_rest_api_client as polarion_api +from lxml import html + +from capella2polarion.converters import polarion_html_helper as html_helper + + +class TextWorkItemProvider: + """Class providing text work items, their generation and insertion.""" + + def __init__( + self, + text_work_item_id_field: str = html_helper.TEXT_WORK_ITEM_ID_FIELD, + text_work_item_type: str = html_helper.TEXT_WORK_ITEM_TYPE, + existing_text_work_items: list[polarion_api.WorkItem] | None = None, + ): + self.old_text_work_items: dict[str, polarion_api.WorkItem] = {} + for work_item in existing_text_work_items or []: + # We only use those work items which have an ID defined by us + if text_id := work_item.additional_attributes.get( + text_work_item_id_field + ): + if text_id in self.old_text_work_items: + raise ValueError( + f"There are multiple text work items with " + f"{text_work_item_id_field} == {text_id}" + ) + + self.old_text_work_items[text_id] = work_item + + self.text_work_item_id_field = text_work_item_id_field + self.text_work_item_type = text_work_item_type + self.new_text_work_items: dict[str, polarion_api.WorkItem] = {} + + def generate_text_work_items( + self, + content: list[html.HtmlElement] | str, + work_item_id_filter: list[str] | None = None, + ): + """Generate text work items from the provided html.""" + content = html_helper.ensure_fragments(content) + for element in content: + if element.tag != html_helper.WORK_ITEM_TAG: + continue + + if not (text_id := element.get("id")): + raise ValueError("All work items must have an ID in template") + + if not ( + (work_item := self.old_text_work_items.get(text_id)) + and ( + work_item_id_filter is None + or work_item.id in work_item_id_filter + ) + ): + work_item = polarion_api.WorkItem( + type=self.text_work_item_type, + title="", + status="open", + additional_attributes={ + self.text_work_item_id_field: text_id + }, + ) + + work_item.description_type = "text/html" + inner_content = "".join( + [ + ( + html.tostring(child, encoding="unicode") + if isinstance(child, html.HtmlElement) + else child + ) + for child in element.iterchildren() + ] + ) + if element.text: + inner_content = element.text + inner_content + + work_item.description = inner_content + self.new_text_work_items[text_id] = work_item + + def insert_text_work_items( + self, + document: polarion_api.Document, + ): + """Insert text work items into the given document.""" + if not self.new_text_work_items: + return + + assert document.home_page_content is not None + assert document.rendering_layouts is not None + layout_index = html_helper.get_layout_index( + "paragraph", document.rendering_layouts, self.text_work_item_type + ) + html_fragments = html_helper.ensure_fragments( + document.home_page_content.value + ) + new_content = [] + last_match = -1 + for index, element in enumerate(html_fragments): + if not isinstance(element, html.HtmlElement): + continue + + if element.tag == "workitem": + new_content += html_fragments[last_match + 1 : index] + last_match = index + if work_item := self.new_text_work_items.get( + element.get("id") + ): + new_content.append( + html.fromstring( + html_helper.POLARION_WORK_ITEM_DOCUMENT.format( + pid=work_item.id, + lid=layout_index, + custom_info="", + ) + ) + ) + + new_content += html_fragments[last_match + 1 :] + document.home_page_content.value = "\n".join( + [html.tostring(element).decode("utf-8") for element in new_content] + ) diff --git a/capella2polarion/data_models.py b/capella2polarion/data_models.py index 3b445795..b6dcd3c1 100644 --- a/capella2polarion/data_models.py +++ b/capella2polarion/data_models.py @@ -4,12 +4,15 @@ from __future__ import annotations import base64 +import dataclasses import hashlib import json import typing as t import polarion_rest_api_client as polarion_api +from capella2polarion.converters import text_work_item_provider + class CapellaWorkItem(polarion_api.WorkItem): """A WorkItem class with additional Capella related attributes.""" @@ -60,3 +63,23 @@ def calculate_checksum(self) -> str: | dict(sorted(attachment_checksums.items())) ) return self._checksum + + +@dataclasses.dataclass +class DocumentData: + """A class to store data related to a rendered document.""" + + document: polarion_api.Document + headings: list[polarion_api.WorkItem] + text_work_item_provider: text_work_item_provider.TextWorkItemProvider + + +@dataclasses.dataclass +class DocumentInfo: + """Class for information regarding a document which should be created.""" + + project_id: str | None + module_folder: str + module_name: str + text_work_item_type: str + text_work_item_id_field: str diff --git a/tests/conftest.py b/tests/conftest.py index a1310ecf..296cd368 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -128,8 +128,10 @@ def base_object( ) c2p_cli.setup_logger() - mock_api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) - monkeypatch.setattr(polarion_api, "OpenAPIPolarionProjectClient", mock_api) + mock_api_client = mock.MagicMock(spec=polarion_api.PolarionClient) + monkeypatch.setattr(polarion_api, "PolarionClient", mock_api_client) + mock_project_client = mock.MagicMock(spec=polarion_api.ProjectClient) + monkeypatch.setattr(polarion_api, "ProjectClient", mock_project_client) c2p_cli.config = mock.Mock(converter_config.ConverterConfig) fake = FakeModelObject("uuid1", name="Fake 1") @@ -173,8 +175,10 @@ def base_object( @pytest.fixture def empty_polarion_worker(monkeypatch: pytest.MonkeyPatch): - mock_api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) - monkeypatch.setattr(polarion_api, "OpenAPIPolarionProjectClient", mock_api) + mock_api_client = mock.MagicMock(spec=polarion_api.PolarionClient) + monkeypatch.setattr(polarion_api, "PolarionClient", mock_api_client) + mock_project_client = mock.MagicMock(spec=polarion_api.ProjectClient) + monkeypatch.setattr(polarion_api, "ProjectClient", mock_project_client) polarion_params = polarion_worker.PolarionWorkerParams( project_id="project_id", url=TEST_HOST, @@ -182,3 +186,7 @@ def empty_polarion_worker(monkeypatch: pytest.MonkeyPatch): delete_work_items=True, ) yield polarion_worker.CapellaPolarionWorker(polarion_params) + + +DOCUMENT_TEMPLATES = TEST_DOCUMENT_ROOT / "templates" +DOCUMENT_TEXT_WORK_ITEMS = "document_work_items.html.j2" diff --git a/tests/data/documents/combined_config.yaml b/tests/data/documents/combined_config.yaml index 6a1034b3..3660a6d2 100644 --- a/tests/data/documents/combined_config.yaml +++ b/tests/data/documents/combined_config.yaml @@ -37,6 +37,21 @@ mixed_authority: section_params: section1: param_1: Test + - template_directory: jupyter-notebooks/document_templates + sections: + section1: test-icd.html.j2 + section2: test-icd.html.j2 + heading_numbering: True + project_id: TestProject + status_allow_list: + - draft + - open + instances: + - polarion_space: _default + polarion_name: id1239 + section_params: + section1: + param_1: Test full_authority: - template_directory: jupyter-notebooks/document_templates template: test-icd.html.j2 @@ -66,3 +81,14 @@ full_authority: instances: - polarion_space: _default polarion_name: id1238 + - template_directory: jupyter-notebooks/document_templates + template: test-icd.html.j2 + project_id: TestProject + status_allow_list: + - draft + - open + instances: + - polarion_space: _default + polarion_name: id1240 + params: + interface: 2681f26a-e492-4e5d-8b33-92fb00a48622 diff --git a/tests/data/documents/full_authority_config.yaml b/tests/data/documents/full_authority_config.yaml index e56aca98..fdbb9899 100644 --- a/tests/data/documents/full_authority_config.yaml +++ b/tests/data/documents/full_authority_config.yaml @@ -3,6 +3,10 @@ - template_directory: jupyter-notebooks/document_templates template: test-icd.html.j2 + project_id: TestProject + status_allow_list: + - draft + - open instances: - polarion_space: _default polarion_name: id123 diff --git a/tests/data/documents/mixed_config.yaml b/tests/data/documents/mixed_config.yaml index a0d30924..f3ca0586 100644 --- a/tests/data/documents/mixed_config.yaml +++ b/tests/data/documents/mixed_config.yaml @@ -3,6 +3,10 @@ mixed_authority: - template_directory: jupyter-notebooks/document_templates + project_id: TestProject + status_allow_list: + - draft + - open sections: section1: test-icd.html.j2 section2: test-icd.html.j2 @@ -21,6 +25,8 @@ mixed_authority: section1: test-icd.html.j2 section2: test-icd.html.j2 heading_numbering: True + text_work_item_type: myType + text_work_item_id_field: myId work_item_layouts: componentExchange: fields_at_start: diff --git a/tests/data/documents/sections/section1.html.j2 b/tests/data/documents/sections/section1.html.j2 index 575220b9..13b1afb7 100644 --- a/tests/data/documents/sections/section1.html.j2 +++ b/tests/data/documents/sections/section1.html.j2 @@ -6,3 +6,4 @@ {{ heading(3, "New Heading", session) }}

    {{ global_param }}

    {{ local_param }}

    +TestContent diff --git a/tests/data/documents/sections/section2.html.j2 b/tests/data/documents/sections/section2.html.j2 index ec7a3fb7..0e0630a0 100644 --- a/tests/data/documents/sections/section2.html.j2 +++ b/tests/data/documents/sections/section2.html.j2 @@ -6,3 +6,4 @@ {{ heading(3, "Keep Heading", session) }}

    Overwritten: {{ global_param }}

    {{ local_param }}

    +TestContent diff --git a/tests/data/documents/templates/document_work_items.html.j2 b/tests/data/documents/templates/document_work_items.html.j2 new file mode 100644 index 00000000..aecd905a --- /dev/null +++ b/tests/data/documents/templates/document_work_items.html.j2 @@ -0,0 +1,10 @@ + + +This is Text in a text workitem + +Text +
    12
    +
    diff --git a/tests/test_cli.py b/tests/test_cli.py index 6d7516f6..7894f405 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -23,8 +23,10 @@ def test_migrate_model_elements(monkeypatch: pytest.MonkeyPatch): - mock_api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) - monkeypatch.setattr(polarion_api, "OpenAPIPolarionProjectClient", mock_api) + mock_api_client = mock.MagicMock(spec=polarion_api.PolarionClient) + monkeypatch.setattr(polarion_api, "PolarionClient", mock_api_client) + mock_project_client = mock.MagicMock(spec=polarion_api.ProjectClient) + monkeypatch.setattr(polarion_api, "ProjectClient", mock_project_client) mock_get_polarion_wi_map = mock.MagicMock() monkeypatch.setattr( polarion_worker.CapellaPolarionWorker, @@ -86,8 +88,10 @@ def test_migrate_model_elements(monkeypatch: pytest.MonkeyPatch): def test_render_documents(monkeypatch: pytest.MonkeyPatch): - mock_api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) - monkeypatch.setattr(polarion_api, "OpenAPIPolarionProjectClient", mock_api) + mock_api_client = mock.MagicMock(spec=polarion_api.PolarionClient) + monkeypatch.setattr(polarion_api, "PolarionClient", mock_api_client) + mock_project_client = mock.MagicMock(spec=polarion_api.ProjectClient) + monkeypatch.setattr(polarion_api, "ProjectClient", mock_project_client) mock_get_polarion_wi_map = mock.MagicMock() monkeypatch.setattr( polarion_worker.CapellaPolarionWorker, @@ -95,7 +99,7 @@ def test_render_documents(monkeypatch: pytest.MonkeyPatch): mock_get_polarion_wi_map, ) mock_get_document = mock.MagicMock() - mock_get_document.side_effect = lambda folder, name: ( + mock_get_document.side_effect = lambda folder, name, project_id: ( polarion_api.Document( module_folder=folder, module_name=name, @@ -113,11 +117,11 @@ def test_render_documents(monkeypatch: pytest.MonkeyPatch): "get_document", mock_get_document, ) - mock_post_documents = mock.MagicMock() + mock_create_documents = mock.MagicMock() monkeypatch.setattr( polarion_worker.CapellaPolarionWorker, - "post_documents", - mock_post_documents, + "create_documents", + mock_create_documents, ) mock_update_documents = mock.MagicMock() monkeypatch.setattr( @@ -125,12 +129,6 @@ def test_render_documents(monkeypatch: pytest.MonkeyPatch): "update_documents", mock_update_documents, ) - mock_update_work_items = mock.MagicMock() - monkeypatch.setattr( - polarion_worker.CapellaPolarionWorker, - "update_work_items", - mock_update_work_items, - ) command: list[str] = [ "--polarion-project-id", @@ -151,10 +149,26 @@ def test_render_documents(monkeypatch: pytest.MonkeyPatch): assert result.exit_code == 0 assert mock_get_polarion_wi_map.call_count == 1 - assert mock_get_document.call_count == 6 - assert mock_post_documents.call_count == 1 - assert len(mock_post_documents.call_args.args[0]) == 1 - assert mock_update_documents.call_count == 1 - assert len(mock_update_documents.call_args.args[0]) == 1 - assert mock_update_work_items.call_count == 1 - assert len(mock_update_work_items.call_args.args[0]) == 1 + assert mock_get_document.call_count == 8 + assert [call.args[2] for call in mock_get_document.call_args_list] == [ + None, + None, + None, + "TestProject", + None, + None, + None, + "TestProject", + ] + + assert mock_create_documents.call_count == 2 + assert len(mock_create_documents.call_args_list[0].args[0]) == 1 + assert len(mock_create_documents.call_args_list[1].args[0]) == 1 + assert mock_create_documents.call_args_list[0].args[1] is None + assert mock_create_documents.call_args_list[1].args[1] == "TestProject" + + assert mock_update_documents.call_count == 2 + assert len(mock_update_documents.call_args_list[0].args[0]) == 1 + assert len(mock_update_documents.call_args_list[1].args[0]) == 0 + assert mock_update_documents.call_args_list[0].args[1] is None + assert mock_update_documents.call_args_list[1].args[1] == "TestProject" diff --git a/tests/test_documents.py b/tests/test_documents.py index 9c56332e..5069b46d 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -6,9 +6,18 @@ from lxml import etree, html from capella2polarion import data_models as dm -from capella2polarion.connectors import polarion_worker -from capella2polarion.converters import document_config, document_renderer -from tests.conftest import TEST_COMBINED_DOCUMENT_CONFIG, TEST_DOCUMENT_ROOT +from capella2polarion.connectors import polarion_repo, polarion_worker +from capella2polarion.converters import ( + document_config, + document_renderer, + text_work_item_provider, +) +from tests.conftest import ( + DOCUMENT_TEMPLATES, + DOCUMENT_TEXT_WORK_ITEMS, + TEST_COMBINED_DOCUMENT_CONFIG, + TEST_DOCUMENT_ROOT, +) CLASSES_TEMPLATE = "test-classes.html.j2" JUPYTER_TEMPLATE_FOLDER = "jupyter-notebooks/document_templates" @@ -19,28 +28,60 @@ MIXED_AUTHORITY_DOCUMENT = TEST_DOCUMENT_ROOT / "mixed_authority_doc.html" -def existing_documents() -> dict[tuple[str, str], polarion_api.Document]: +def existing_documents() -> polarion_repo.DocumentRepository: return { - ("_default", "id123"): polarion_api.Document( - module_folder="_default", - module_name="id123", - home_page_content=polarion_api.TextContent( - type="text/html", - value=MIXED_AUTHORITY_DOCUMENT.read_text("utf-8"), + (None, "_default", "id123"): ( + polarion_api.Document( + module_folder="_default", + module_name="id123", + status="draft", + home_page_content=polarion_api.TextContent( + type="text/html", + value=MIXED_AUTHORITY_DOCUMENT.read_text("utf-8"), + ), + rendering_layouts=[ + polarion_api.RenderingLayout( + "Class", "paragraph", type="class" + ) + ], ), - rendering_layouts=[ - polarion_api.RenderingLayout( - "Class", "paragraph", type="class" - ) - ], + [], + ), + (None, "_default", "id1237"): ( + polarion_api.Document( + module_folder="_default", + module_name="id1237", + status="draft", + home_page_content=polarion_api.TextContent( + type="text/html", + value=MIXED_AUTHORITY_DOCUMENT.read_text("utf-8"), + ), + ), + [], + ), + ("TestProject", "_default", "id1239"): ( + polarion_api.Document( + module_folder="_default", + module_name="id1239", + status="in_review", + home_page_content=polarion_api.TextContent( + type="text/html", + value=MIXED_AUTHORITY_DOCUMENT.read_text("utf-8"), + ), + ), + [], ), - ("_default", "id1237"): polarion_api.Document( - module_folder="_default", - module_name="id1237", - home_page_content=polarion_api.TextContent( - type="text/html", - value=MIXED_AUTHORITY_DOCUMENT.read_text("utf-8"), + ("TestProject", "_default", "id1240"): ( + polarion_api.Document( + module_folder="_default", + module_name="id1240", + status="draft", + home_page_content=polarion_api.TextContent( + type="text/html", + value=MIXED_AUTHORITY_DOCUMENT.read_text("utf-8"), + ), ), + [], ), } @@ -67,7 +108,7 @@ def test_create_new_document( empty_polarion_worker.polarion_data_repo, model ) - new_doc, wis = renderer.render_document( + document_data = renderer.render_document( JUPYTER_TEMPLATE_FOLDER, CLASSES_TEMPLATE, "_default", @@ -76,10 +117,10 @@ def test_create_new_document( ) content: list[etree._Element] = html.fromstring( - new_doc.home_page_content.value + document_data.document.home_page_content.value ) - assert len(wis) == 0 - assert new_doc.rendering_layouts == [ + assert len(document_data.headings) == 0 + assert document_data.document.rendering_layouts == [ polarion_api.RenderingLayout( label="Class", type="class", layouter="section" ) @@ -138,18 +179,19 @@ def test_update_document( ), ) - new_doc, wis = renderer.render_document( + document_data = renderer.render_document( JUPYTER_TEMPLATE_FOLDER, CLASSES_TEMPLATE, document=old_doc, + text_work_items={}, cls="c710f1c2-ede6-444e-9e2b-0ff30d7fd040", ) content: list[etree._Element] = html.fromstring( - new_doc.home_page_content.value + document_data.document.home_page_content.value ) - assert len(new_doc.rendering_layouts) == 1 - assert new_doc.rendering_layouts[ + assert len(document_data.document.rendering_layouts) == 1 + assert document_data.document.rendering_layouts[ 0 ].properties == polarion_api.data_models.RenderingProperties( fields_at_start=["ID"] @@ -160,9 +202,9 @@ def test_update_document( assert content[0].tag == "h1" assert content[1].text == "Data Classes" assert content[1].tag == "h2" - assert len(wis) == 1 - assert wis[0].id == "ATSY-16062" - assert wis[0].title == "Class Document" + assert len(document_data.headings) == 1 + assert document_data.headings[0].id == "ATSY-16062" + assert document_data.headings[0].title == "Class Document" def test_mixed_authority_document( @@ -180,7 +222,7 @@ def test_mixed_authority_document( ), ) - new_doc, wis = renderer.update_mixed_authority_document( + document_data = renderer.update_mixed_authority_document( old_doc, DOCUMENT_SECTIONS, { @@ -195,28 +237,160 @@ def test_mixed_authority_document( "global_param": "Overwrite global param", }, }, + text_work_item_provider=text_work_item_provider.TextWorkItemProvider( + "MyField", + "MyType", + [ + polarion_api.WorkItem( + id="EXISTING", additional_attributes={"MyField": "id1"} + ) + ], + ), ) content: list[etree._Element] = html.fromstring( - new_doc.home_page_content.value + document_data.document.home_page_content.value ) - assert len(content) == 15 + assert len(document_data.text_work_item_provider.new_text_work_items) == 2 + assert ( + document_data.text_work_item_provider.new_text_work_items["id1"].id + is None + ) + assert ( + document_data.text_work_item_provider.new_text_work_items["id2"].id + is None + ) + assert len(content) == 17 assert [c.tag for c in content[:3]] == ["h1", "p", "p"] assert (c4 := content[4]).tag == "h3" and c4.text == "New Heading" assert content[5].text == "Global Test" assert content[6].text == "Local Test section 1" - assert content[8].text == "This will be kept." - assert content[10].get("id") == ( + assert content[9].text == "This will be kept." + assert content[11].get("id") == ( "polarion_wiki macro name=module-workitem;params=id=ATSY-18305" ) - assert content[10].tag == "h3" - assert content[11].text == "Overwritten: Overwrite global param" - assert content[12].text == "Local Test section 2" - assert content[14].text == "Some postfix stuff" - assert len(wis) == 1 - assert wis[0].id == "ATSY-18305" - assert wis[0].title == "Keep Heading" + assert content[11].tag == "h3" + assert content[12].text == "Overwritten: Overwrite global param" + assert content[13].text == "Local Test section 2" + assert content[16].text == "Some postfix stuff" + assert len(document_data.headings) == 1 + assert document_data.headings[0].id == "ATSY-18305" + assert document_data.headings[0].title == "Keep Heading" + + +def test_create_full_authority_document_text_work_items( + empty_polarion_worker: polarion_worker.CapellaPolarionWorker, + model: capellambse.MelodyModel, +): + renderer = document_renderer.DocumentRenderer( + empty_polarion_worker.polarion_data_repo, model + ) + + document_data = renderer.render_document( + DOCUMENT_TEMPLATES, + DOCUMENT_TEXT_WORK_ITEMS, + "_default", + "TEST-DOC", + text_work_item_provider=text_work_item_provider.TextWorkItemProvider( + "MyField", + "MyType", + ), + ) + + assert len(document_data.text_work_item_provider.new_text_work_items) == 2 + assert ( + document_data.text_work_item_provider.new_text_work_items["id1"].id + is None + ) + assert ( + document_data.text_work_item_provider.new_text_work_items["id1"].type + == "MyType" + ) + assert ( + document_data.text_work_item_provider.new_text_work_items[ + "id1" + ].additional_attributes["MyField"] + == "id1" + ) + assert ( + document_data.text_work_item_provider.new_text_work_items["id2"].id + is None + ) + assert ( + document_data.text_work_item_provider.new_text_work_items["id2"].type + == "MyType" + ) + assert ( + document_data.text_work_item_provider.new_text_work_items[ + "id2" + ].additional_attributes["MyField"] + == "id2" + ) + + +def test_update_full_authority_document_text_work_items( + empty_polarion_worker: polarion_worker.CapellaPolarionWorker, + model: capellambse.MelodyModel, +): + renderer = document_renderer.DocumentRenderer( + empty_polarion_worker.polarion_data_repo, model + ) + old_doc = polarion_api.Document( + module_folder="_default", + module_name="TEST-DOC", + home_page_content=polarion_api.TextContent( + type="text/html", + value="", + ), + ) + + document_data = renderer.render_document( + DOCUMENT_TEMPLATES, + DOCUMENT_TEXT_WORK_ITEMS, + "_default", + "TEST-DOC", + document=old_doc, + text_work_item_provider=text_work_item_provider.TextWorkItemProvider( + "MyField", + "MyType", + [ + polarion_api.WorkItem( + id="EXISTING", additional_attributes={"MyField": "id1"} + ) + ], + ), + ) + + assert len(document_data.text_work_item_provider.new_text_work_items) == 2 + assert ( + document_data.text_work_item_provider.new_text_work_items["id1"].id + == "EXISTING" + ) + assert ( + document_data.text_work_item_provider.new_text_work_items["id1"].type + is None + ) + assert ( + document_data.text_work_item_provider.new_text_work_items[ + "id1" + ].additional_attributes["MyField"] + == "id1" + ) + assert ( + document_data.text_work_item_provider.new_text_work_items["id2"].id + is None + ) + assert ( + document_data.text_work_item_provider.new_text_work_items["id2"].type + == "MyType" + ) + assert ( + document_data.text_work_item_provider.new_text_work_items[ + "id2" + ].additional_attributes["MyField"] + == "id2" + ) def test_render_all_documents_partially_successfully( @@ -231,24 +405,49 @@ def test_render_all_documents_partially_successfully( empty_polarion_worker.polarion_data_repo, model ) - new_docs, updated_docs, work_items = renderer.render_documents( - conf, existing_documents() - ) + projects_data = renderer.render_documents(conf, existing_documents()) - # There are 6 documents in the config, we expect 3 rendering to fail - assert len(caplog.records) == 3 + # There are 8 documents in the config, we expect 4 rendering to fail + assert len(caplog.records) == 4 + # The first tree documents weren't rendered due to an error, the fourth + # wasn't rendered because of status restrictions, which is a just warning + assert [lr.levelno for lr in caplog.records] == [40, 40, 40, 30] # For one valid config we did not pass a document, so we expect a new one - assert len(new_docs) == 1 - # And two updated documents - assert len(updated_docs) == 2 - # In both existing documents we had 2 headings. In full authority mode + assert len(projects_data[None].new_docs) == 1 + # And three updated documents + assert len(projects_data[None].updated_docs) == 2 + assert len(projects_data["TestProject"].updated_docs) == 1 + # In all existing documents we had 2 headings. In full authority mode # both should be updated and in mixed authority mode only one of them as # the other is outside the rendering area - assert len(work_items) == 3 - assert len(updated_docs[0].rendering_layouts) == 0 - assert len(updated_docs[1].rendering_layouts) == 1 - assert updated_docs[0].outline_numbering is None - assert updated_docs[1].outline_numbering is None + assert ( + sum( + len(document_data.headings) + for document_data in projects_data[None].updated_docs + ) + == 3 + ) + assert ( + sum( + len(document_data.headings) + for document_data in projects_data["TestProject"].updated_docs + ) + == 2 + ) + assert ( + len(projects_data[None].updated_docs[0].document.rendering_layouts) + == 0 + ) + assert ( + len(projects_data[None].updated_docs[1].document.rendering_layouts) + == 1 + ) + assert ( + projects_data[None].updated_docs[0].document.outline_numbering is None + ) + assert ( + projects_data[None].updated_docs[1].document.outline_numbering is None + ) def test_render_all_documents_overwrite_headings_layouts( @@ -262,12 +461,13 @@ def test_render_all_documents_overwrite_headings_layouts( empty_polarion_worker.polarion_data_repo, model, True, True ) - _, updated_docs, _ = renderer.render_documents(conf, existing_documents()) + projects_data = renderer.render_documents(conf, existing_documents()) + updated_docs = projects_data[None].updated_docs - assert len(updated_docs[0].rendering_layouts) == 2 - assert len(updated_docs[1].rendering_layouts) == 2 - assert updated_docs[0].outline_numbering is False - assert updated_docs[1].outline_numbering is False + assert len(updated_docs[0].document.rendering_layouts) == 2 + assert len(updated_docs[1].document.rendering_layouts) == 2 + assert updated_docs[0].document.outline_numbering is False + assert updated_docs[1].document.outline_numbering is False def test_full_authority_document_config(): @@ -289,6 +489,10 @@ def test_full_authority_document_config(): assert conf.full_authority[0].instances[0].params == { "interface": "3d21ab4b-7bf6-428b-ba4c-a27bca4e86db" } + assert conf.full_authority[0].project_id == "TestProject" + assert conf.full_authority[0].status_allow_list == ["draft", "open"] + assert conf.full_authority[1].project_id is None + assert conf.full_authority[1].status_allow_list is None def test_mixed_authority_document_config(): @@ -308,6 +512,8 @@ def test_mixed_authority_document_config(): assert len(conf.mixed_authority[0].instances) == 2 assert conf.mixed_authority[0].instances[0].polarion_space == "_default" assert conf.mixed_authority[0].instances[0].polarion_name == "id123" + assert conf.mixed_authority[0].project_id == "TestProject" + assert conf.mixed_authority[0].status_allow_list == ["draft", "open"] assert conf.mixed_authority[0].instances[0].polarion_title == "Interface23" assert conf.mixed_authority[0].instances[0].params == { "interface": "3d21ab4b-7bf6-428b-ba4c-a27bca4e86db" @@ -315,14 +521,20 @@ def test_mixed_authority_document_config(): assert conf.mixed_authority[1].instances[0].section_params == { "section1": {"param_1": "Test"} } + assert conf.mixed_authority[1].project_id is None + assert conf.mixed_authority[1].status_allow_list is None + assert conf.mixed_authority[0].text_work_item_type == "text" + assert conf.mixed_authority[0].text_work_item_id_field == "__C2P__id" + assert conf.mixed_authority[1].text_work_item_type == "myType" + assert conf.mixed_authority[1].text_work_item_id_field == "myId" def test_combined_config(): with open(TEST_COMBINED_DOCUMENT_CONFIG, "r", encoding="utf-8") as f: conf = document_config.read_config_file(f) - assert len(conf.full_authority) == 2 - assert len(conf.mixed_authority) == 2 + assert len(conf.full_authority) == 3 + assert len(conf.mixed_authority) == 3 def test_rendering_config(): diff --git a/tests/test_elements.py b/tests/test_elements.py index fc19f1c5..1c8e88eb 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -254,15 +254,6 @@ def test_create_diagrams(diagr_base_object: BaseObjectContainer): cls="diagram", ) - @staticmethod - def test_create_diagrams_filters_non_diagram_elements( - diagr_base_object: BaseObjectContainer, - ): - # This test does not make any sense, but it also didn't before - pw = diagr_base_object.pw - diagr_base_object.mc.generate_work_items(pw.polarion_data_repo) - assert pw.client.generate_work_items.call_count == 0 - @staticmethod def test_delete_diagrams(diagr_base_object: BaseObjectContainer): pw = diagr_base_object.pw @@ -270,10 +261,13 @@ def test_delete_diagrams(diagr_base_object: BaseObjectContainer): diagr_base_object.mc.generate_work_items(pw.polarion_data_repo) pw.create_missing_work_items(diagr_base_object.mc.converter_session) pw.delete_orphaned_work_items(diagr_base_object.mc.converter_session) - assert pw.client is not None - assert pw.client.delete_work_items.call_count == 1 - assert pw.client.delete_work_items.call_args[0][0] == ["Diag-1"] - assert pw.client.generate_work_items.call_count == 0 + assert pw.project_client is not None + assert pw.project_client.work_items.delete.call_count == 1 + assert ( + pw.project_client.work_items.delete.call_args[0][0][0].id + == "Diag-1" + ) + assert pw.project_client.work_items.create.call_count == 0 class TestModelElements: @@ -755,8 +749,8 @@ def test_update_work_items( polarion_api_get_all_work_items = mock.MagicMock() polarion_api_get_all_work_items.return_value = polarion_work_item_list monkeypatch.setattr( - base_object.pw.client, - "get_all_work_items", + base_object.pw.project_client.work_items, + "get_all", polarion_api_get_all_work_items, ) @@ -778,24 +772,35 @@ def test_update_work_items( get_work_item_mock = mock.MagicMock() get_work_item_mock.return_value = polarion_work_item_list[0] monkeypatch.setattr( - base_object.pw.client, - "get_work_item", + base_object.pw.project_client.work_items, + "get", get_work_item_mock, ) base_object.pw.compare_and_update_work_items( base_object.mc.converter_session ) - assert base_object.pw.client is not None - assert base_object.pw.client.get_all_work_item_links.call_count == 0 - assert base_object.pw.client.delete_work_item_links.call_count == 0 - assert base_object.pw.client.create_work_item_links.call_count == 0 - assert base_object.pw.client.update_work_item.call_count == 1 - assert base_object.pw.client.get_work_item.call_count == 1 assert ( - base_object.pw.client.get_all_work_item_attachments.call_count == 0 + base_object.pw.project_client.work_items.links.get_all.call_count + == 0 ) - work_item = base_object.pw.client.update_work_item.call_args[0][0] + assert ( + base_object.pw.project_client.work_items.links.delete.call_count + == 0 + ) + assert ( + base_object.pw.project_client.work_items.links.create.call_count + == 0 + ) + assert base_object.pw.project_client.work_items.update.call_count == 1 + assert base_object.pw.project_client.work_items.get.call_count == 1 + assert ( + base_object.pw.project_client.work_items.attachments.get_all.call_count # pylint: disable=line-too-long + == 0 + ) + work_item = base_object.pw.project_client.work_items.update.call_args[ + 0 + ][0] assert isinstance(work_item, data_models.CapellaWorkItem) assert work_item.id == "Obj-1" assert work_item.title == "Fake 1" @@ -821,8 +826,8 @@ def test_update_deleted_work_item( polarion_api_get_all_work_items = mock.MagicMock() polarion_api_get_all_work_items.return_value = polarion_work_item_list monkeypatch.setattr( - base_object.pw.client, - "get_all_work_items", + base_object.pw.project_client.work_items, + "get_all", polarion_api_get_all_work_items, ) @@ -846,24 +851,26 @@ def test_update_deleted_work_item( get_work_item_mock = mock.MagicMock() get_work_item_mock.return_value = polarion_work_item_list[0] monkeypatch.setattr( - base_object.pw.client, - "get_work_item", + base_object.pw.project_client.work_items, + "get", get_work_item_mock, ) base_object.pw.delete_orphaned_work_items( base_object.mc.converter_session ) - assert base_object.pw.client.update_work_item.called is False + assert base_object.pw.project_client.work_items.update.called is False base_object.pw.create_missing_work_items( base_object.mc.converter_session ) - assert base_object.pw.client.create_work_items.called is False + assert base_object.pw.project_client.work_items.create.called is False base_object.pw.compare_and_update_work_items( base_object.mc.converter_session ) - work_item = base_object.pw.client.update_work_item.call_args[0][0] + work_item = base_object.pw.project_client.work_items.update.call_args[ + 0 + ][0] assert isinstance(work_item, data_models.CapellaWorkItem) assert work_item.status == "open" @@ -897,8 +904,7 @@ def test_update_work_items_filters_work_items_with_same_checksum( base_object.mc.converter_session ) - assert base_object.pw.client is not None - assert base_object.pw.client.update_work_item.call_count == 0 + assert base_object.pw.project_client.work_items.update.call_count == 0 @staticmethod def test_update_work_items_same_checksum_force( @@ -931,8 +937,7 @@ def test_update_work_items_same_checksum_force( base_object.mc.converter_session ) - assert base_object.pw.client is not None - assert base_object.pw.client.update_work_item.call_count == 1 + assert base_object.pw.project_client.work_items.update.call_count == 1 @staticmethod def test_update_links_with_no_elements(base_object: BaseObjectContainer): @@ -944,7 +949,10 @@ def test_update_links_with_no_elements(base_object: BaseObjectContainer): base_object.mc.converter_session ) - assert base_object.pw.client.get_all_work_item_links.call_count == 0 + assert ( + base_object.pw.project_client.work_items.links.get_all.call_count + == 0 + ) @staticmethod def test_update_links(base_object: BaseObjectContainer): @@ -980,8 +988,7 @@ def test_update_links(base_object: BaseObjectContainer): ) ) - assert base_object.pw.client is not None - base_object.pw.client.get_all_work_item_links.side_effect = ( + base_object.pw.project_client.work_items.links.get_all.side_effect = ( [link], [], ) @@ -1001,7 +1008,7 @@ def test_update_links(base_object: BaseObjectContainer): work_item_1.linked_work_items_truncated = True work_item_2.linked_work_items_truncated = True - base_object.pw.client.get_work_item.side_effect = ( + base_object.pw.project_client.work_items.get.side_effect = ( work_item_1, work_item_2, ) @@ -1009,19 +1016,31 @@ def test_update_links(base_object: BaseObjectContainer): base_object.pw.compare_and_update_work_items( base_object.mc.converter_session ) - assert base_object.pw.client is not None - links = base_object.pw.client.get_all_work_item_links.call_args_list - assert base_object.pw.client.get_all_work_item_links.call_count == 2 + links = ( + base_object.pw.project_client.work_items.links.get_all.call_args_list # pylint: disable=line-too-long + ) + assert ( + base_object.pw.project_client.work_items.links.get_all.call_count + == 2 + ) assert [links[0][0][0], links[1][0][0]] == ["Obj-1", "Obj-2"] - new_links = base_object.pw.client.create_work_item_links.call_args[0][ - 0 - ] - assert base_object.pw.client.create_work_item_links.call_count == 1 + new_links = ( + base_object.pw.project_client.work_items.links.create.call_args[0][ + 0 + ] + ) + assert ( + base_object.pw.project_client.work_items.links.create.call_count + == 1 + ) assert new_links == [expected_new_link] - assert base_object.pw.client.delete_work_item_links.call_count == 1 - assert base_object.pw.client.delete_work_item_links.call_args[0][ + assert ( + base_object.pw.project_client.work_items.links.delete.call_count + == 1 + ) + assert base_object.pw.project_client.work_items.links.delete.call_args[ 0 - ] == [link] + ][0] == [link] @staticmethod def test_patch_work_item_grouped_links( @@ -1095,9 +1114,8 @@ def mock_back_link(converter_data, back_links): base_object.pw.compare_and_update_work_items( base_object.mc.converter_session ) - assert base_object.pw.client is not None update_work_item_calls = ( - base_object.pw.client.update_work_item.call_args_list + base_object.pw.project_client.work_items.update.call_args_list ) assert len(update_work_item_calls) == 3 mock_grouped_links_calls = mock_grouped_links.call_args_list diff --git a/tests/test_polarion_worker_documents.py b/tests/test_polarion_worker_documents.py new file mode 100644 index 00000000..714847bb --- /dev/null +++ b/tests/test_polarion_worker_documents.py @@ -0,0 +1,142 @@ +# Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 +from unittest import mock + +import polarion_rest_api_client as polarion_api + +from capella2polarion import data_models +from capella2polarion.connectors import polarion_worker +from capella2polarion.converters import text_work_item_provider + +from .conftest import DOCUMENT_TEMPLATES, DOCUMENT_TEXT_WORK_ITEMS + + +def _set_work_item_id(work_items: list[polarion_api.WorkItem]): + for index, work_item in enumerate(work_items): + work_item.id = f"id{index}" + + +def test_update_document( + empty_polarion_worker: polarion_worker.CapellaPolarionWorker, +): + path = DOCUMENT_TEMPLATES / DOCUMENT_TEXT_WORK_ITEMS + document = polarion_api.Document( + module_folder="_default", + module_name="TEST-DOC", + rendering_layouts=[], + home_page_content=polarion_api.TextContent( + type="text/html", + value=path.read_text("utf-8"), + ), + ) + document_data = data_models.DocumentData( + document, + [], + text_work_item_provider.TextWorkItemProvider( + "MyField", + "MyType", + [ + polarion_api.WorkItem( + id="EXISTING", additional_attributes={"MyField": "id1"} + ), + ], + ), + ) + document_data.text_work_item_provider.generate_text_work_items( + document.home_page_content.value + ) + client = empty_polarion_worker.project_client + client.work_items.create.side_effect = _set_work_item_id + + empty_polarion_worker.update_documents([document_data]) + + assert document.home_page_content.value.endswith( + '
    \n' + '
    ' + ) + assert client.documents.update.call_count == 1 + assert client.documents.update.call_args.args[0] == [document] + assert client.work_items.create.call_count == 1 + assert len(client.work_items.create.call_args.args[0]) == 1 + assert client.work_items.update.call_count == 2 + assert len(client.work_items.update.call_args_list[0].args[0]) == 1 + assert len(client.work_items.update.call_args_list[1].args[0]) == 0 + + +def test_create_document( + empty_polarion_worker: polarion_worker.CapellaPolarionWorker, +): + path = DOCUMENT_TEMPLATES / DOCUMENT_TEXT_WORK_ITEMS + document = polarion_api.Document( + module_folder="_default", + module_name="TEST-DOC", + rendering_layouts=[], + home_page_content=polarion_api.TextContent( + type="text/html", + value=path.read_text("utf-8"), + ), + ) + document_data = data_models.DocumentData( + document, + [], + text_work_item_provider.TextWorkItemProvider( + "MyField", + "MyType", + ), + ) + document_data.text_work_item_provider.generate_text_work_items( + document.home_page_content.value + ) + client = empty_polarion_worker.project_client + client.work_items.create.side_effect = _set_work_item_id + + empty_polarion_worker.update_documents([document_data]) + + assert document.home_page_content.value.endswith( + '
    \n' + '
    ' + ) + assert client.documents.update.call_count == 1 + assert client.documents.update.call_args.args[0] == [document] + assert client.work_items.create.call_count == 1 + assert len(client.work_items.create.call_args.args[0]) == 2 + assert client.work_items.update.call_count == 2 + assert len(client.work_items.update.call_args_list[0].args[0]) == 0 + assert len(client.work_items.update.call_args_list[1].args[0]) == 0 + + +def test_use_correct_client( + empty_polarion_worker: polarion_worker.CapellaPolarionWorker, +): + empty_polarion_worker.project_client = mock.MagicMock() + document = polarion_api.Document( + module_folder="_default", + module_name="TEST-DOC-A", + rendering_layouts=[], + home_page_content=polarion_api.TextContent( + type="text/html", + value="", + ), + ) + + document_data = data_models.DocumentData( + document, + [], + text_work_item_provider.TextWorkItemProvider(), + ) + + empty_polarion_worker.create_documents([document_data], "OtherProject") + empty_polarion_worker.update_documents([document_data], "OtherProject") + + assert len(empty_polarion_worker.project_client.method_calls) == 0 + assert len(empty_polarion_worker._additional_clients) == 1 + assert ( + client := empty_polarion_worker._additional_clients.get("OtherProject") + ) + assert client.documents.update.call_count == 1 + assert client.documents.create.call_count == 1 + assert client.work_items.update.call_count == 1 diff --git a/tests/test_workitem_attachments.py b/tests/test_workitem_attachments.py index 6e51889e..d9a07977 100644 --- a/tests/test_workitem_attachments.py +++ b/tests/test_workitem_attachments.py @@ -57,8 +57,10 @@ @pytest.fixture def worker(monkeypatch: pytest.MonkeyPatch): - mock_api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) - monkeypatch.setattr(polarion_api, "OpenAPIPolarionProjectClient", mock_api) + mock_api_client = mock.MagicMock(spec=polarion_api.PolarionClient) + monkeypatch.setattr(polarion_api, "PolarionClient", mock_api_client) + mock_project_client = mock.MagicMock(spec=polarion_api.ProjectClient) + monkeypatch.setattr(polarion_api, "ProjectClient", mock_project_client) return polarion_worker.CapellaPolarionWorker( polarion_worker.PolarionWorkerParams( "TEST", @@ -118,11 +120,13 @@ def test_diagram_attachments_new( [data_models.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID)] ) - worker.client.get_work_item.return_value = data_models.CapellaWorkItem( - WORKITEM_ID, uuid_capella=TEST_DIAG_UUID + worker.project_client.work_items.get.return_value = ( + data_models.CapellaWorkItem(WORKITEM_ID, uuid_capella=TEST_DIAG_UUID) + ) + worker.project_client.work_items.attachments = mock.MagicMock() + worker.project_client.work_items.attachments.create.side_effect = ( + set_attachment_ids ) - worker.client.create_work_item_attachments = mock.MagicMock() - worker.client.create_work_item_attachments.side_effect = set_attachment_ids converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( "", @@ -136,15 +140,15 @@ def test_diagram_attachments_new( converter.converter_session[TEST_DIAG_UUID] ) - assert worker.client.update_work_item.call_count == 1 - assert worker.client.create_work_item_attachments.call_count == 1 - assert worker.client.get_all_work_item_attachments.call_count == 0 + assert worker.project_client.work_items.update.call_count == 1 + assert worker.project_client.work_items.attachments.create.call_count == 1 + assert worker.project_client.work_items.attachments.get_all.call_count == 0 created_attachments: list[polarion_api.WorkItemAttachment] = ( - worker.client.create_work_item_attachments.call_args.args[0] + worker.project_client.work_items.attachments.create.call_args.args[0] ) work_item: data_models.CapellaWorkItem = ( - worker.client.update_work_item.call_args.args[0] + worker.project_client.work_items.update.call_args.args[0] ) assert len(created_attachments) == 2 @@ -180,11 +184,15 @@ def test_new_diagram( ] ) - worker.client.get_work_item.return_value = data_models.CapellaWorkItem( - WORKITEM_ID, uuid_capella=TEST_DIAG_UUID, checksum=checksum + worker.project_client.work_items.get.return_value = ( + data_models.CapellaWorkItem( + WORKITEM_ID, uuid_capella=TEST_DIAG_UUID, checksum=checksum + ) + ) + worker.project_client.work_items.attachments.create = mock.MagicMock() + worker.project_client.work_items.attachments.create.side_effect = ( + set_attachment_ids ) - worker.client.create_work_item_attachments = mock.MagicMock() - worker.client.create_work_item_attachments.side_effect = set_attachment_ids converter.converter_session[TEST_DIAG_UUID] = data_session.ConverterData( "", @@ -198,9 +206,9 @@ def test_new_diagram( converter.converter_session[TEST_DIAG_UUID] ) - assert worker.client.update_work_item.call_count == 1 - assert worker.client.create_work_item_attachments.call_count == 1 - assert worker.client.update_work_item.call_args.args[ + assert worker.project_client.work_items.update.call_count == 1 + assert worker.project_client.work_items.attachments.create.call_count == 1 + assert worker.project_client.work_items.update.call_args.args[ 0 ].description == TEST_DIAG_DESCR.format( title="Diagram", @@ -233,14 +241,16 @@ def test_diagram_attachments_updated( ), ] - worker.client.get_work_item.return_value = data_models.CapellaWorkItem( - WORKITEM_ID, - uuid_capella=TEST_DIAG_UUID, - attachments=existing_attachments, + worker.project_client.work_items.get.return_value = ( + data_models.CapellaWorkItem( + WORKITEM_ID, + uuid_capella=TEST_DIAG_UUID, + attachments=existing_attachments, + ) ) - worker.client.get_all_work_item_attachments = mock.MagicMock() - worker.client.get_all_work_item_attachments.return_value = ( + worker.project_client.work_items.attachments.get_all = mock.MagicMock() + worker.project_client.work_items.attachments.get_all.return_value = ( existing_attachments ) @@ -256,13 +266,13 @@ def test_diagram_attachments_updated( converter.converter_session[TEST_DIAG_UUID] ) - assert worker.client.update_work_item.call_count == 1 - assert worker.client.create_work_item_attachments.call_count == 0 - assert worker.client.update_work_item_attachment.call_count == 2 - assert worker.client.get_all_work_item_attachments.call_count == 1 + assert worker.project_client.work_items.update.call_count == 1 + assert worker.project_client.work_items.attachments.create.call_count == 0 + assert worker.project_client.work_items.attachments.update.call_count == 2 + assert worker.project_client.work_items.attachments.get_all.call_count == 1 work_item: data_models.CapellaWorkItem = ( - worker.client.update_work_item.call_args.args[0] + worker.project_client.work_items.update.call_args.args[0] ) assert work_item.description == TEST_DIAG_DESCR.format( @@ -292,8 +302,8 @@ def test_diagram_attachments_unchanged_work_item_changed( ) ] ) - worker.client.get_all_work_item_attachments = mock.MagicMock() - worker.client.get_all_work_item_attachments.return_value = [ + worker.project_client.work_items.attachments.get_all = mock.MagicMock() + worker.project_client.work_items.attachments.get_all.return_value = [ polarion_api.WorkItemAttachment( WORKITEM_ID, "SVG-ATTACHMENT", @@ -320,12 +330,12 @@ def test_diagram_attachments_unchanged_work_item_changed( converter.converter_session[TEST_DIAG_UUID] ) - assert worker.client.update_work_item.call_count == 1 - assert worker.client.create_work_item_attachments.call_count == 0 - assert worker.client.update_work_item_attachment.call_count == 0 + assert worker.project_client.work_items.update.call_count == 1 + assert worker.project_client.work_items.attachments.create.call_count == 0 + assert worker.project_client.work_items.attachments.update.call_count == 0 work_item: data_models.CapellaWorkItem = ( - worker.client.update_work_item.call_args.args[0] + worker.project_client.work_items.update.call_args.args[0] ) assert work_item.description == TEST_DIAG_DESCR.format( @@ -363,10 +373,10 @@ def test_diagram_attachments_fully_unchanged( converter.converter_session[TEST_DIAG_UUID] ) - assert worker.client.update_work_item.call_count == 0 - assert worker.client.create_work_item_attachments.call_count == 0 - assert worker.client.update_work_item_attachment.call_count == 0 - assert worker.client.get_all_work_item_attachments.call_count == 0 + assert worker.project_client.work_items.update.call_count == 0 + assert worker.project_client.work_items.attachments.create.call_count == 0 + assert worker.project_client.work_items.attachments.update.call_count == 0 + assert worker.project_client.work_items.attachments.get_all.call_count == 0 def test_add_context_diagram( @@ -385,21 +395,23 @@ def test_add_context_diagram( model.by_uuid(uuid), ) - worker.client.create_work_item_attachments = mock.MagicMock() - worker.client.create_work_item_attachments.side_effect = set_attachment_ids + worker.project_client.work_items.attachments.create = mock.MagicMock() + worker.project_client.work_items.attachments.create.side_effect = ( + set_attachment_ids + ) converter.generate_work_items(worker.polarion_data_repo, False, True) worker.compare_and_update_work_item(converter.converter_session[uuid]) - assert worker.client.update_work_item.call_count == 1 - assert worker.client.create_work_item_attachments.call_count == 1 + assert worker.project_client.work_items.update.call_count == 1 + assert worker.project_client.work_items.attachments.create.call_count == 1 created_attachments: list[polarion_api.WorkItemAttachment] = ( - worker.client.create_work_item_attachments.call_args.args[0] + worker.project_client.work_items.attachments.create.call_args.args[0] ) work_item: data_models.CapellaWorkItem = ( - worker.client.update_work_item.call_args.args[0] + worker.project_client.work_items.update.call_args.args[0] ) assert len(created_attachments) == 2 @@ -439,8 +451,8 @@ def test_diagram_delete_attachments( ) ] ) - worker.client.get_all_work_item_attachments = mock.MagicMock() - worker.client.get_all_work_item_attachments.return_value = [ + worker.project_client.work_items.attachments.get_all = mock.MagicMock() + worker.project_client.work_items.attachments.get_all.return_value = [ polarion_api.WorkItemAttachment( WORKITEM_ID, "SVG-ATTACHMENT", @@ -473,13 +485,13 @@ def test_diagram_delete_attachments( converter.converter_session[TEST_DIAG_UUID] ) - assert worker.client.update_work_item.call_count == 1 - assert worker.client.create_work_item_attachments.call_count == 0 - assert worker.client.update_work_item_attachment.call_count == 0 - assert worker.client.delete_work_item_attachment.call_count == 2 + assert worker.project_client.work_items.update.call_count == 1 + assert worker.project_client.work_items.attachments.create.call_count == 0 + assert worker.project_client.work_items.attachments.update.call_count == 0 + assert worker.project_client.work_items.attachments.delete.call_count == 2 work_item: data_models.CapellaWorkItem = ( - worker.client.update_work_item.call_args.args[0] + worker.project_client.work_items.update.call_args.args[0] ) assert work_item.description is None